From 731eb4853780858cd19dd117219fc7befc0f8722 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Thu, 26 Mar 2026 11:31:40 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20CRUD=20=D0=BA=D1=83=D1=80=D1=81=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ CourseController (resource) ✅ CoursePolicy ✅ Маршруты: /admin/courses ✅ Blade-шаблоны: - index.blade.php (список с карточками) - create.blade.php (форма создания) - edit.blade.php (форма редактирования) - show.blade.php (просмотр) ✅ Ссылка в сайдбаре ✅ Загрузка изображений (thumbnail) ✅ Типы курсов: standard, scorm, h5p Co-authored-by: Qwen-Coder --- .../Controllers/Admin/CourseController.php | 148 ++++++++++++++++++ app/Models/Course.php | 5 + app/Policies/CoursePolicy.php | 34 ++++ app/Providers/AuthServiceProvider.php | 1 + .../views/admin/courses/create.blade.php | 89 +++++++++++ resources/views/admin/courses/edit.blade.php | 87 ++++++++++ resources/views/admin/courses/index.blade.php | 80 ++++++++++ resources/views/admin/courses/show.blade.php | 76 +++++++++ resources/views/partials/_sidebar.blade.php | 2 +- routes/web.php | 2 + 10 files changed, 523 insertions(+), 1 deletion(-) create mode 100755 app/Http/Controllers/Admin/CourseController.php create mode 100755 app/Policies/CoursePolicy.php create mode 100644 resources/views/admin/courses/create.blade.php create mode 100644 resources/views/admin/courses/edit.blade.php create mode 100644 resources/views/admin/courses/index.blade.php create mode 100644 resources/views/admin/courses/show.blade.php diff --git a/app/Http/Controllers/Admin/CourseController.php b/app/Http/Controllers/Admin/CourseController.php new file mode 100755 index 0000000..d65412c --- /dev/null +++ b/app/Http/Controllers/Admin/CourseController.php @@ -0,0 +1,148 @@ +middleware('auth'); + } + + public function index(Request $request) + { + Gate::authorize('viewAny', Course::class); + + $query = Course::with(['category', 'creator']); + + if ($request->filled('category_id')) { + $query->where('category_id', $request->category_id); + } + + if ($request->filled('type')) { + $query->where('type', $request->type); + } + + if ($request->filled('search')) { + $query->where(function($q) use ($request) { + $q->where('title', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + $courses = $query->orderBy('created_at', 'desc')->paginate(20); + $categories = CourseCategory::pluck('name', 'id'); + + return view('admin.courses.index', compact('courses', 'categories')); + } + + public function create() + { + Gate::authorize('create', Course::class); + + $categories = CourseCategory::pluck('name', 'id'); + + return view('admin.courses.create', compact('categories')); + } + + public function store(Request $request) + { + Gate::authorize('create', Course::class); + + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'slug' => 'nullable|string|max:255|unique:courses', + 'description' => 'nullable|string', + 'objectives' => 'nullable|string', + 'category_id' => 'nullable|exists:course_categories,id', + 'type' => 'required|in:standard,scorm,h5p', + 'thumbnail' => 'nullable|image|max:2048', + 'duration_minutes' => 'nullable|integer', + 'passing_score' => 'nullable|integer|min:0|max:100', + 'has_certificate' => 'boolean', + 'is_active' => 'boolean', + ]); + + $validated['slug'] = $validated['slug'] ?? Str::slug($validated['title']); + $validated['created_by'] = auth()->id(); + $validated['is_active'] = $request->boolean('is_active'); + $validated['has_certificate'] = $request->boolean('has_certificate'); + + if ($request->hasFile('thumbnail')) { + $validated['thumbnail'] = $request->file('thumbnail')->store('courses/thumbnails', 'public'); + } + + $course = Course::create($validated); + + return redirect()->route('admin.courses.show', $course)->with('success', 'Курс успешно создан.'); + } + + public function show(Course $course) + { + Gate::authorize('view', $course); + + $course->load(['category', 'creator', 'modules', 'tests']); + + return view('admin.courses.show', compact('course')); + } + + public function edit(Course $course) + { + Gate::authorize('update', $course); + + $categories = CourseCategory::pluck('name', 'id'); + + return view('admin.courses.edit', compact('course', 'categories')); + } + + public function update(Request $request, Course $course) + { + Gate::authorize('update', $course); + + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'slug' => 'nullable|string|max:255|unique:courses,slug,' . $course->id, + 'description' => 'nullable|string', + 'objectives' => 'nullable|string', + 'category_id' => 'nullable|exists:course_categories,id', + 'type' => 'required|in:standard,scorm,h5p', + 'thumbnail' => 'nullable|image|max:2048', + 'duration_minutes' => 'nullable|integer', + 'passing_score' => 'nullable|integer|min:0|max:100', + 'has_certificate' => 'boolean', + 'is_active' => 'boolean', + ]); + + $validated['slug'] = $validated['slug'] ?? Str::slug($validated['title']); + $validated['is_active'] = $request->boolean('is_active'); + $validated['has_certificate'] = $request->boolean('has_certificate'); + + if ($request->hasFile('thumbnail')) { + if ($course->thumbnail) Storage::disk('public')->delete($course->thumbnail); + $validated['thumbnail'] = $request->file('thumbnail')->store('courses/thumbnails', 'public'); + } + + $course->update($validated); + + return redirect()->route('admin.courses.show', $course)->with('success', 'Курс успешно обновлён.'); + } + + public function destroy(Course $course) + { + Gate::authorize('delete', $course); + + if ($course->thumbnail) Storage::disk('public')->delete($course->thumbnail); + + $course->delete(); + + return redirect()->route('admin.courses.index')->with('success', 'Курс успешно удалён.'); + } +} diff --git a/app/Models/Course.php b/app/Models/Course.php index dc1ca33..ac63bfb 100755 --- a/app/Models/Course.php +++ b/app/Models/Course.php @@ -74,4 +74,9 @@ class Course extends Model { return $this->hasMany(CourseRequestItem::class); } + + public function users() + { + return $this->belongsToMany(User::class, 'course_assignments'); + } } diff --git a/app/Policies/CoursePolicy.php b/app/Policies/CoursePolicy.php new file mode 100755 index 0000000..280cac0 --- /dev/null +++ b/app/Policies/CoursePolicy.php @@ -0,0 +1,34 @@ +hasRole(['Administrator', 'Manager', 'Curator']); + } + + public function view(User $user, Course $course): bool + { + return $user->hasRole(['Administrator', 'Manager', 'Curator']); + } + + public function create(User $user): bool + { + return $user->hasRole(['Administrator', 'Manager', 'Curator']); + } + + public function update(User $user, Course $course): bool + { + return $user->hasRole(['Administrator', 'Manager', 'Curator']); + } + + public function delete(User $user, Course $course): bool + { + return $user->hasRole(['Administrator', 'Manager']); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 7edbf4c..6ae86d1 100755 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -21,6 +21,7 @@ class AuthServiceProvider extends ServiceProvider */ protected $policies = [ CourseCategory::class => CourseCategoryPolicy::class, + Course::class => CoursePolicy::class, Organization::class => OrganizationPolicy::class, Group::class => GroupPolicy::class, User::class => UserPolicy::class, diff --git a/resources/views/admin/courses/create.blade.php b/resources/views/admin/courses/create.blade.php new file mode 100644 index 0000000..ea8e05a --- /dev/null +++ b/resources/views/admin/courses/create.blade.php @@ -0,0 +1,89 @@ +@extends('layouts.app') +@section('title', 'Добавить курс') +@section('content') +
+
+ +
+
+

Добавить курс

+ Назад +
+
+ @csrf +
+
+
+
+
Основная информация
+
+ + + @error('title')
{{ $message }}
@enderror +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
Настройки
+
+ + +
+
+ + + @error('type')
{{ $message }}
@enderror +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + Отмена +
+
+
+
+@endsection diff --git a/resources/views/admin/courses/edit.blade.php b/resources/views/admin/courses/edit.blade.php new file mode 100644 index 0000000..8786d6a --- /dev/null +++ b/resources/views/admin/courses/edit.blade.php @@ -0,0 +1,87 @@ +@extends('layouts.app') +@section('title', 'Редактировать курс') +@section('content') +
+
+ +
+
+

Редактировать: {{ $course->title }}

+ Назад +
+
+ @csrf @method('PUT') +
+
+
+
+
+ + + @error('title')
{{ $message }}
@enderror +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ has_certificate) ? 'checked' : '' }}> + +
+
+ is_active) ? 'checked' : '' }}> + +
+
+ + + @if($course->thumbnail)Текущая: {{ basename($course->thumbnail) }}@endif +
+
+
+
+
+ + Отмена +
+
+
+
+@endsection diff --git a/resources/views/admin/courses/index.blade.php b/resources/views/admin/courses/index.blade.php new file mode 100644 index 0000000..6d342d9 --- /dev/null +++ b/resources/views/admin/courses/index.blade.php @@ -0,0 +1,80 @@ +@extends('layouts.app') +@section('title', 'Курсы') +@section('content') +
+
+ +
+
+

Курсы

+ @can('create', App\Models\Course::class) + Добавить курс + @endcan +
+ @if(session('success'))
{{ session('success') }}
@endif + +
+
+
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+ @forelse($courses as $course) +
+
+ @if($course->thumbnail) + {{ $course->title }} + @else +
+ @endif +
+
{{ $course->title }}
+

{{ Str::limit($course->description, 80) }}

+
+ {{ $course->category?->name ?? 'Без категории' }} + {{ $course->type }} +
+ {{ $course->modules->count() }} модулей +
+ +
+
+ @empty +

Курсов пока нет

+ @endforelse +
+
+
+ {{ $courses->links() }} +
+
+
+@endsection diff --git a/resources/views/admin/courses/show.blade.php b/resources/views/admin/courses/show.blade.php new file mode 100644 index 0000000..3782971 --- /dev/null +++ b/resources/views/admin/courses/show.blade.php @@ -0,0 +1,76 @@ +@extends('layouts.app') +@section('title', $course->title) +@section('content') +
+
+ +
+
+

{{ $course->title }}

+
+ @can('update', $course)Редактировать@endcan + Назад +
+
+
+
+
+ @if($course->thumbnail){{ $course->title }} + @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->description ?? '—' }}
+
+
+
+
+
Цели обучения
+
{{ $course->objectives ?? '—' }}
+
+
+
+
+
+
+
Модули
+
+ @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 +
+
+
+
+
+
+
+@endsection diff --git a/resources/views/partials/_sidebar.blade.php b/resources/views/partials/_sidebar.blade.php index 83d8750..f314e9f 100644 --- a/resources/views/partials/_sidebar.blade.php +++ b/resources/views/partials/_sidebar.blade.php @@ -25,7 +25,7 @@ diff --git a/routes/web.php b/routes/web.php index 3e1356a..a377474 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Admin\OrganizationController; 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\DashboardController; use Illuminate\Support\Facades\Route; @@ -40,5 +41,6 @@ Route::middleware('auth')->group(function () { Route::resource('organizations.groups', GroupController::class); Route::resource('users', UserController::class); Route::resource('course-categories', CourseCategoryController::class); + Route::resource('courses', CourseController::class); }); });