chore: commit remaining workspace changes
This commit is contained in:
308
app/Http/Controllers/Settings/AcademyCourseBuilderController.php
Normal file
308
app/Http/Controllers/Settings/AcademyCourseBuilderController.php
Normal file
@@ -0,0 +1,308 @@
|
||||
<?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]),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user