From 41224069c10c8e66a8f1301ebf1b9d67b76aa470 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Thu, 2 Apr 2026 17:27:49 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=D0=9C=D0=BE=D0=B4=D1=83=D0=BB=D0=B8=20?= =?UTF-8?q?=D0=BA=D1=83=D1=80=D1=81=D0=B0=20(=D0=BA=D0=BE=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D0=BD=D1=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ CourseModuleController (store, update, destroy) ✅ Маршруты для модулей ✅ UI добавления/редактирования модулей ✅ Типы: section, lesson, video, file, link, test ✅ Тесты как тип модуля (выбор из существующих) ✅ Загрузка файлов ✅ Иерархия (родитель/потомки) Co-authored-by: Qwen-Coder --- .../Admin/CourseModuleController.php | 100 +++++ app/Models/CourseModule.php | 11 +- ...418_add_fields_to_course_modules_table.php | 28 ++ resources/views/admin/courses/show.blade.php | 351 +++++++++++++++--- routes/web.php | 4 + 5 files changed, 436 insertions(+), 58 deletions(-) create mode 100755 app/Http/Controllers/Admin/CourseModuleController.php create mode 100755 database/migrations/2026_04_02_092418_add_fields_to_course_modules_table.php diff --git a/app/Http/Controllers/Admin/CourseModuleController.php b/app/Http/Controllers/Admin/CourseModuleController.php new file mode 100755 index 0000000..a01dd82 --- /dev/null +++ b/app/Http/Controllers/Admin/CourseModuleController.php @@ -0,0 +1,100 @@ +middleware('auth'); + } + + public function store(Request $request, Course $course) + { + Gate::authorize('update', $course); + + $validated = $request->validate([ + 'parent_id' => 'nullable|exists:course_modules,id', + 'title' => 'required|string|max:255', + 'type' => 'required|in:section,lesson,video,file,link,test', + 'content' => 'nullable|string', + 'video_url' => 'nullable|url', + 'file_path' => 'nullable|string', + 'external_url' => 'nullable|url', + 'test_id' => 'nullable|exists:tests,id', + 'sort_order' => 'nullable|integer', + 'duration_minutes' => 'nullable|integer', + 'is_required' => 'boolean', + 'is_active' => 'boolean', + ]); + + $validated['course_id'] = $course->id; + $validated['is_required'] = $request->boolean('is_required'); + $validated['is_active'] = $request->boolean('is_active'); + + // Загрузка файла + if ($request->hasFile('file')) { + $validated['file_path'] = $request->file('file')->store('course-files', 'public'); + } + + CourseModule::create($validated); + + return back()->with('success', 'Модуль добавлен'); + } + + public function update(Request $request, Course $course, CourseModule $module) + { + Gate::authorize('update', $course); + + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'type' => 'required|in:section,lesson,video,file,link,test', + 'content' => 'nullable|string', + 'video_url' => 'nullable|url', + 'external_url' => 'nullable|url', + 'test_id' => 'nullable|exists:tests,id', + 'sort_order' => 'nullable|integer', + 'duration_minutes' => 'nullable|integer', + 'is_required' => 'boolean', + 'is_active' => 'boolean', + ]); + + $validated['is_required'] = $request->boolean('is_required'); + $validated['is_active'] = $request->boolean('is_active'); + + // Загрузка нового файла + if ($request->hasFile('file')) { + // Удаляем старый файл + if ($module->file_path) { + Storage::disk('public')->delete($module->file_path); + } + $validated['file_path'] = $request->file('file')->store('course-files', 'public'); + } + + $module->update($validated); + + return back()->with('success', 'Модуль обновлён'); + } + + public function destroy(Course $course, CourseModule $module) + { + Gate::authorize('update', $course); + + // Удаляем файл если есть + if ($module->file_path) { + Storage::disk('public')->delete($module->file_path); + } + + $module->delete(); + + return back()->with('success', 'Модуль удалён'); + } +} diff --git a/app/Models/CourseModule.php b/app/Models/CourseModule.php index 559f402..49817e7 100755 --- a/app/Models/CourseModule.php +++ b/app/Models/CourseModule.php @@ -16,11 +16,15 @@ class CourseModule extends Model 'parent_id', 'title', 'content', - 'type', + 'type', // section, lesson, video, file, link, test 'sort_order', 'duration_minutes', 'is_required', 'is_active', + 'video_url', + 'file_path', + 'external_url', + 'test_id', // Связь с тестом ]; protected $casts = [ @@ -48,6 +52,11 @@ class CourseModule extends Model return $this->hasMany(Test::class); } + public function test(): BelongsTo + { + return $this->belongsTo(Test::class); + } + public function userProgress(): HasMany { return $this->hasMany(UserCourseProgress::class); diff --git a/database/migrations/2026_04_02_092418_add_fields_to_course_modules_table.php b/database/migrations/2026_04_02_092418_add_fields_to_course_modules_table.php new file mode 100755 index 0000000..4475235 --- /dev/null +++ b/database/migrations/2026_04_02_092418_add_fields_to_course_modules_table.php @@ -0,0 +1,28 @@ +

{{ $course->title }}

- @can('update', $course)Редактировать@endcan + Редактировать Назад
-
-
+ + @if(session('success'))
{{ session('success') }}
@endif + +
+
- @if($course->thumbnail){{ $course->title }} - @else
@endif +
+
Категория: {{ $course->category?->name ?? '—' }}
+
Slug: {{ $course->slug ?? '—' }}
+
Статус: @if($course->is_active)Активен@elseНе активен@endif
+
-
-
+
+
- - - - - - - - - -
Категория:{{ $course->category?->name ?? '—' }}
Тип:{{ $course->type }}
Длительность:{{ $course->duration_minutes ?? '—' }} мин
Проходной балл:{{ $course->passing_score }}%
Сертификат:@if($course->has_certificate)Да@elseНет@endif
Статус:@if($course->is_active)Активен@elseНе активен@endif
Создан:{{ $course->created_at->format('d.m.Y H:i') }}
Автор:{{ $course->creator?->name ?? '—' }}
+
Модулей: {{ $course->modules()->whereNull('parent_id')->count() }}
+
Тестов: {{ $course->tests->count() }}
-
-
-
-
Описание
-
{{ $course->description ?? '—' }}
-
+ + +
+
+
Модули курса
+
-
-
-
Цели обучения
-
{{ $course->objectives ?? '—' }}
+
+ @if($course->modules()->whereNull('parent_id')->count() > 0) +
+ @foreach($course->modules()->whereNull('parent_id')->orderBy('sort_order')->get() as $module) +
+

+ +

+
+
+
+
+ @if($module->duration_minutes) {{ $module->duration_minutes }} мин.@endif + @if($module->is_required)Обязательный@endif +
+
+ +
+ @csrf @method('DELETE') + +
+
+
+ + @if($module->content)

{{ $module->content }}

@endif + @if($module->video_url)@endif + @if($module->file_path)@endif + @if($module->external_url)@endif + @if($module->test_id)
Тест: {{ $module->test?->title }}
@endif + + + @if($module->children()->count() > 0) +
+ @foreach($module->children()->orderBy('sort_order')->get() as $child) +
+
+ {{ $child->type }} + {{ $child->title }} +
+
+ +
+ @csrf @method('DELETE') + +
+
+
+ @endforeach +
+ @endif + + + +
+
+
+ @endforeach
+ @else +

Модулей пока нет. Добавьте первый модуль!

+ @endif
-
-
-
-
Модули
-
- @if($course->modules->count() > 0)
    @foreach($course->modules as $module)
  • {{ $module->title }}{{ $module->type }}
  • @endforeach
- @else

Нет модулей

@endif -
-
+ + +
+ -
-
-
-
Тесты
- -
-
- @if($course->tests->count() > 0) -
    - @foreach($course->tests as $test) -
  • - {{ $test->title }} - {{ $test->type }} -
  • - @endforeach -
- @else -

Нет тестов

- @endif -
-
+
+ @if($course->tests->count() > 0) +
    + @foreach($course->tests as $test) +
  • + {{ $test->title }} +
    + {{ $test->type }} + +
    +
  • + @endforeach +
+ @else +

Тестов пока нет

+ @endif
+ + + + +@push('scripts') + +@endpush @endsection diff --git a/routes/web.php b/routes/web.php index ff753bc..930e8f6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -8,6 +8,7 @@ use App\Http\Controllers\Admin\GroupController; use App\Http\Controllers\Admin\UserController; use App\Http\Controllers\Admin\CourseCategoryController; use App\Http\Controllers\Admin\CourseController; +use App\Http\Controllers\Admin\CourseModuleController; use App\Http\Controllers\Admin\TestController; use App\Http\Controllers\Admin\QuestionController; use App\Http\Controllers\Admin\CourseAssignmentController; @@ -63,6 +64,9 @@ Route::middleware('auth')->group(function () { Route::resource('course-categories', CourseCategoryController::class); Route::resource('courses', CourseController::class); Route::resource('courses.tests', TestController::class); + Route::post('/courses/{course}/modules', [CourseModuleController::class, 'store'])->name('courses.modules.store'); + Route::put('/courses/{course}/modules/{module}', [CourseModuleController::class, 'update'])->name('courses.modules.update'); + Route::delete('/courses/{course}/modules/{module}', [CourseModuleController::class, 'destroy'])->name('courses.modules.destroy'); Route::resource('tests.questions', QuestionController::class); Route::resource('course-assignments', CourseAssignmentController::class)->except(['show', 'edit', 'update']); Route::get('/course-assignments/{course}', CourseAssignmentController::class . '@show')->name('course-assignments.show');