Feat: CRUD курсов
✅ 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 <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
ca95621856
commit
731eb48537
|
|
@ -0,0 +1,148 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Course;
|
||||
use App\Models\CourseCategory;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CourseController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->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', 'Курс успешно удалён.');
|
||||
}
|
||||
}
|
||||
|
|
@ -74,4 +74,9 @@ class Course extends Model
|
|||
{
|
||||
return $this->hasMany(CourseRequestItem::class);
|
||||
}
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'course_assignments');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Course;
|
||||
use App\Models\User;
|
||||
|
||||
class CoursePolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->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']);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
@extends('layouts.app')
|
||||
@section('title', 'Добавить курс')
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav class="col-md-3 col-lg-2 d-md-block sidebar"><div class="position-sticky pt-3">@include('partials._sidebar')</div></nav>
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Добавить курс</h1>
|
||||
<a href="{{ route('admin.courses.index') }}" class="btn btn-secondary btn-sm">Назад</a>
|
||||
</div>
|
||||
<form action="{{ route('admin.courses.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<h5>Основная информация</h5>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Название *</label>
|
||||
<input type="text" name="title" class="form-control @error('title') is-invalid @enderror" value="{{ old('title') }}" required>
|
||||
@error('title')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Slug (URL)</label>
|
||||
<input type="text" name="slug" class="form-control" value="{{ old('slug') }}" placeholder="автоматически">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Описание</label>
|
||||
<textarea name="description" class="form-control" rows="3">{{ old('description') }}</textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Цели обучения</label>
|
||||
<textarea name="objectives" class="form-control" rows="2">{{ old('objectives') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<h5>Настройки</h5>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Категория</label>
|
||||
<select name="category_id" class="form-select">
|
||||
<option value="">Без категории</option>
|
||||
@foreach($categories as $id => $name)<option value="{{ $id }}" {{ old('category_id') == $id ? 'selected' : '' }}>{{ $name }}</option>@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Тип *</label>
|
||||
<select name="type" class="form-select">
|
||||
<option value="standard" {{ old('type') == 'standard' ? 'selected' : '' }}>Стандартный</option>
|
||||
<option value="scorm" {{ old('type') == 'scorm' ? 'selected' : '' }}>SCORM</option>
|
||||
<option value="h5p" {{ old('type') == 'h5p' ? 'selected' : '' }}>H5P</option>
|
||||
</select>
|
||||
@error('type')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Длительность (мин)</label>
|
||||
<input type="number" name="duration_minutes" class="form-control" value="{{ old('duration_minutes', 60) }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Проходной балл (%)</label>
|
||||
<input type="number" name="passing_score" class="form-control" value="{{ old('passing_score', 70) }}" min="0" max="100">
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" name="has_certificate" value="1" class="form-check-input" {{ old('has_certificate') ? 'checked' : '' }}>
|
||||
<label class="form-check-label">Выдавать сертификат</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" name="is_active" value="1" class="form-check-input" {{ old('is_active', true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label">Активен</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Обложка</label>
|
||||
<input type="file" name="thumbnail" class="form-control" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Создать курс</button>
|
||||
<a href="{{ route('admin.courses.index') }}" class="btn btn-secondary">Отмена</a>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
@extends('layouts.app')
|
||||
@section('title', 'Редактировать курс')
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav class="col-md-3 col-lg-2 d-md-block sidebar"><div class="position-sticky pt-3">@include('partials._sidebar')</div></nav>
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Редактировать: {{ $course->title }}</h1>
|
||||
<a href="{{ route('admin.courses.show', $course) }}" class="btn btn-secondary btn-sm">Назад</a>
|
||||
</div>
|
||||
<form action="{{ route('admin.courses.update', $course) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf @method('PUT')
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Название *</label>
|
||||
<input type="text" name="title" class="form-control @error('title') is-invalid @enderror" value="{{ old('title', $course->title) }}" required>
|
||||
@error('title')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Slug</label>
|
||||
<input type="text" name="slug" class="form-control" value="{{ old('slug', $course->slug) }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Описание</label>
|
||||
<textarea name="description" class="form-control" rows="3">{{ old('description', $course->description) }}</textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Цели обучения</label>
|
||||
<textarea name="objectives" class="form-control" rows="2">{{ old('objectives', $course->objectives) }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Категория</label>
|
||||
<select name="category_id" class="form-select">
|
||||
<option value="">Без категории</option>
|
||||
@foreach($categories as $id => $name)<option value="{{ $id }}" {{ old('category_id', $course->category_id) == $id ? 'selected' : '' }}>{{ $name }}</option>@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Тип *</label>
|
||||
<select name="type" class="form-select">
|
||||
<option value="standard" {{ old('type', $course->type) == 'standard' ? 'selected' : '' }}>Стандартный</option>
|
||||
<option value="scorm" {{ old('type', $course->type) == 'scorm' ? 'selected' : '' }}>SCORM</option>
|
||||
<option value="h5p" {{ old('type', $course->type) == 'h5p' ? 'selected' : '' }}>H5P</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Длительность (мин)</label>
|
||||
<input type="number" name="duration_minutes" class="form-control" value="{{ old('duration_minutes', $course->duration_minutes) }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Проходной балл (%)</label>
|
||||
<input type="number" name="passing_score" class="form-control" value="{{ old('passing_score', $course->passing_score) }}" min="0" max="100">
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" name="has_certificate" value="1" class="form-check-input" {{ old('has_certificate', $course->has_certificate) ? 'checked' : '' }}>
|
||||
<label class="form-check-label">Выдавать сертификат</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" name="is_active" value="1" class="form-check-input" {{ old('is_active', $course->is_active) ? 'checked' : '' }}>
|
||||
<label class="form-check-label">Активен</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Обложка</label>
|
||||
<input type="file" name="thumbnail" class="form-control" accept="image/*">
|
||||
@if($course->thumbnail)<small class="text-muted">Текущая: {{ basename($course->thumbnail) }}</small>@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||
<a href="{{ route('admin.courses.show', $course) }}" class="btn btn-secondary">Отмена</a>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
@extends('layouts.app')
|
||||
@section('title', 'Курсы')
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav class="col-md-3 col-lg-2 d-md-block sidebar"><div class="position-sticky pt-3">@include('partials._sidebar')</div></nav>
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Курсы</h1>
|
||||
@can('create', App\Models\Course::class)
|
||||
<a href="{{ route('admin.courses.create') }}" class="btn btn-primary btn-sm"><i class="bi bi-plus-lg"></i> Добавить курс</a>
|
||||
@endcan
|
||||
</div>
|
||||
@if(session('success'))<div class="alert alert-success">{{ session('success') }}</div>@endif
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<form action="{{ route('admin.courses.index') }}" method="GET" class="row g-3">
|
||||
<div class="col-md-4"><input type="text" name="search" class="form-control" placeholder="Поиск..." value="{{ request('search') }}"></div>
|
||||
<div class="col-md-3">
|
||||
<select name="category_id" class="form-select">
|
||||
<option value="">Все категории</option>
|
||||
@foreach($categories as $id => $name)<option value="{{ $id }}" {{ request('category_id') == $id ? 'selected' : '' }}>{{ $name }}</option>@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="type" class="form-select">
|
||||
<option value="">Все типы</option>
|
||||
<option value="standard" {{ request('type') == 'standard' ? 'selected' : '' }}>Стандартный</option>
|
||||
<option value="scorm" {{ request('type') == 'scorm' ? 'selected' : '' }}>SCORM</option>
|
||||
<option value="h5p" {{ request('type') == 'h5p' ? 'selected' : '' }}>H5P</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2"><button type="submit" class="btn btn-primary w-100"><i class="bi bi-search"></i></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
@forelse($courses as $course)
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card h-100 shadow-sm">
|
||||
@if($course->thumbnail)
|
||||
<img src="{{ asset('storage/' . $course->thumbnail) }}" class="card-img-top" alt="{{ $course->title }}" style="height:200px;object-fit:cover;">
|
||||
@else
|
||||
<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height:200px;"><i class="bi bi-book text-white" style="font-size:4rem;"></i></div>
|
||||
@endif
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ $course->title }}</h5>
|
||||
<p class="card-text text-muted small">{{ Str::limit($course->description, 80) }}</p>
|
||||
<div class="mb-2">
|
||||
<span class="badge bg-info">{{ $course->category?->name ?? 'Без категории' }}</span>
|
||||
<span class="badge bg-secondary">{{ $course->type }}</span>
|
||||
</div>
|
||||
<small class="text-muted">{{ $course->modules->count() }} модулей</small>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<div class="btn-group btn-group-sm w-100">
|
||||
<a href="{{ route('admin.courses.show', $course) }}" class="btn btn-outline-primary"><i class="bi bi-eye"></i></a>
|
||||
@can('update', $course)<a href="{{ route('admin.courses.edit', $course) }}" class="btn btn-outline-warning"><i class="bi bi-pencil"></i></a>@endcan
|
||||
@can('delete', $course)
|
||||
<form action="{{ route('admin.courses.destroy', $course) }}" method="POST" class="d-inline" onsubmit="return confirm('Удалить?')">@csrf @method('DELETE')<button class="btn btn-outline-danger"><i class="bi bi-trash"></i></button></form>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-12 text-center text-muted py-5"><i class="bi bi-inbox" style="font-size:3rem;"></i><p class="mt-3">Курсов пока нет</p></div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ $courses->links() }}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
@extends('layouts.app')
|
||||
@section('title', $course->title)
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav class="col-md-3 col-lg-2 d-md-block sidebar"><div class="position-sticky pt-3">@include('partials._sidebar')</div></nav>
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">{{ $course->title }}</h1>
|
||||
<div>
|
||||
@can('update', $course)<a href="{{ route('admin.courses.edit', $course) }}" class="btn btn-warning btn-sm me-2">Редактировать</a>@endcan
|
||||
<a href="{{ route('admin.courses.index') }}" class="btn btn-secondary btn-sm">Назад</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
@if($course->thumbnail)<img src="{{ asset('storage/' . $course->thumbnail) }}" class="card-img-top" alt="{{ $course->title }}">
|
||||
@else<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height:200px;"><i class="bi bi-book text-white" style="font-size:4rem;"></i></div>@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8 mb-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<table class="table table-sm">
|
||||
<tr><th>Категория:</th><td>{{ $course->category?->name ?? '—' }}</td></tr>
|
||||
<tr><th>Тип:</th><td><span class="badge bg-info">{{ $course->type }}</span></td></tr>
|
||||
<tr><th>Длительность:</th><td>{{ $course->duration_minutes ?? '—' }} мин</td></tr>
|
||||
<tr><th>Проходной балл:</th><td>{{ $course->passing_score }}%</td></tr>
|
||||
<tr><th>Сертификат:</th><td>@if($course->has_certificate)<span class="badge bg-success">Да</span>@else<span class="badge bg-secondary">Нет</span>@endif</td></tr>
|
||||
<tr><th>Статус:</th><td>@if($course->is_active)<span class="badge bg-success">Активен</span>@else<span class="badge bg-secondary">Не активен</span>@endif</td></tr>
|
||||
<tr><th>Создан:</th><td>{{ $course->created_at->format('d.m.Y H:i') }}</td></tr>
|
||||
<tr><th>Автор:</th><td>{{ $course->creator?->name ?? '—' }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header"><h5 class="mb-0">Описание</h5></div>
|
||||
<div class="card-body">{{ $course->description ?? '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header"><h5 class="mb-0">Цели обучения</h5></div>
|
||||
<div class="card-body">{{ $course->objectives ?? '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between"><h5 class="mb-0">Модули</h5><a href="#" class="btn btn-sm btn-primary"><i class="bi bi-plus"></i></a></div>
|
||||
<div class="card-body">
|
||||
@if($course->modules->count() > 0)<ul class="list-group list-group-flush">@foreach($course->modules as $module)<li class="list-group-item d-flex justify-content-between">{{ $module->title }}<small class="text-muted">{{ $module->type }}</small></li>@endforeach</ul>
|
||||
@else<p class="text-muted mb-0">Нет модулей</p>@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between"><h5 class="mb-0">Тесты</h5><a href="#" class="btn btn-sm btn-primary"><i class="bi bi-plus"></i></a></div>
|
||||
<div class="card-body">
|
||||
@if($course->tests->count() > 0)<ul class="list-group list-group-flush">@foreach($course->tests as $test)<li class="list-group-item d-flex justify-content-between">{{ $test->title }}<span class="badge bg-secondary">{{ $test->type }}</span></li>@endforeach</ul>
|
||||
@else<p class="text-muted mb-0">Нет тестов</p>@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<a class="nav-link {{ Str::startsWith($currentRoute, 'admin.courses') ? 'active' : '' }}" href="{{ route('admin.courses.index') }}">
|
||||
<i class="bi bi-book"></i> Курсы
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue