diff --git a/app/Http/Controllers/Admin/CourseRequestController.php b/app/Http/Controllers/Admin/CourseRequestController.php
new file mode 100755
index 0000000..58ff44e
--- /dev/null
+++ b/app/Http/Controllers/Admin/CourseRequestController.php
@@ -0,0 +1,282 @@
+middleware('auth');
+ }
+
+ public function index(Request $request)
+ {
+ Gate::authorize('viewAny', CourseRequest::class);
+
+ $query = CourseRequest::with(['organization', 'requestedBy', 'approvedBy', 'items.course']);
+
+ if ($request->filled('status')) {
+ $query->where('status', $request->status);
+ }
+
+ if ($request->filled('organization_id')) {
+ $query->where('organization_id', $request->organization_id);
+ }
+
+ $requests = $query->orderBy('created_at', 'desc')->paginate(20);
+ $organizations = Organization::pluck('name', 'id');
+
+ return view('admin.course-requests.index', compact('requests', 'organizations'));
+ }
+
+ public function create()
+ {
+ Gate::authorize('create', CourseRequest::class);
+
+ $courses = Course::pluck('title', 'id');
+ $users = User::pluck('name', 'id');
+ $groups = Group::pluck('name', 'id');
+ $organizations = Organization::pluck('name', 'id');
+
+ return view('admin.course-requests.create', compact('courses', 'users', 'groups', 'organizations'));
+ }
+
+ public function store(Request $request)
+ {
+ Gate::authorize('create', CourseRequest::class);
+
+ $validated = $request->validate([
+ 'organization_id' => 'required|exists:organizations,id',
+ 'status' => 'nullable|in:pending,approved,rejected',
+ 'note' => 'nullable|string',
+ 'items' => 'required|array',
+ 'items.*.course_id' => 'required|exists:courses,id',
+ 'items.*.user_id' => 'nullable|exists:users,id',
+ 'items.*.group_id' => 'nullable|exists:groups,id',
+ 'items.*.start_date' => 'required|date',
+ 'items.*.end_date' => 'nullable|date|after:items.*.start_date',
+ ]);
+
+ // Определяем статус
+ $user = auth()->user();
+ if ($validated['status'] === 'approved' || $user->hasRole(['Administrator', 'Manager', 'Curator'])) {
+ $status = 'approved';
+ } else {
+ $status = 'pending';
+ }
+
+ DB::beginTransaction();
+
+ try {
+ // Создаём заявку
+ $courseRequest = CourseRequest::create([
+ 'organization_id' => $validated['organization_id'],
+ 'requested_by_user_id' => $user->id,
+ 'status' => $status,
+ 'note' => $validated['note'] ?? null,
+ 'approved_by' => $status === 'approved' ? $user->id : null,
+ 'approved_at' => $status === 'approved' ? now() : null,
+ ]);
+
+ // Создаём элементы заявки
+ foreach ($validated['items'] as $itemData) {
+ CourseRequestItem::create([
+ 'course_request_id' => $courseRequest->id,
+ 'course_id' => $itemData['course_id'],
+ 'user_id' => $itemData['user_id'] ?? null,
+ 'group_id' => $itemData['group_id'] ?? null,
+ 'start_date' => $itemData['start_date'],
+ 'end_date' => $itemData['end_date'] ?? null,
+ ]);
+ }
+
+ // Если заявка одобрена - сразу создаём назначения
+ if ($status === 'approved') {
+ $this->createAssignments($courseRequest);
+ }
+
+ DB::commit();
+
+ return redirect()->route('admin.course-requests.index')
+ ->with('success', $status === 'approved' ? 'Заявка создана и одобрена. Назначения созданы.' : 'Заявка создана и ожидает подтверждения.');
+
+ } catch (\Exception $e) {
+ DB::rollBack();
+ return back()->withErrors(['error' => 'Ошибка при создании заявки: ' . $e->getMessage()])->withInput();
+ }
+ }
+
+ public function show(CourseRequest $courseRequest)
+ {
+ Gate::authorize('view', $courseRequest);
+
+ $courseRequest->load(['organization', 'requestedBy', 'approvedBy', 'items.course', 'items.user', 'items.group']);
+
+ return view('admin.course-requests.show', compact('courseRequest'));
+ }
+
+ public function edit(CourseRequest $courseRequest)
+ {
+ Gate::authorize('update', $courseRequest);
+
+ if ($courseRequest->isApproved() || $courseRequest->isRejected()) {
+ return back()->with('error', 'Нельзя редактировать ' . ($courseRequest->isApproved() ? 'одобренную' : 'отклонённую') . ' заявку.');
+ }
+
+ $courses = Course::pluck('title', 'id');
+ $users = User::pluck('name', 'id');
+ $groups = Group::pluck('name', 'id');
+
+ return view('admin.course-requests.edit', compact('courseRequest', 'courses', 'users', 'groups'));
+ }
+
+ public function update(Request $request, CourseRequest $courseRequest)
+ {
+ Gate::authorize('update', $courseRequest);
+
+ if ($courseRequest->isApproved() || $courseRequest->isRejected()) {
+ return back()->with('error', 'Нельзя редактировать ' . ($courseRequest->isApproved() ? 'одобренную' : 'отклонённую') . ' заявку.');
+ }
+
+ $validated = $request->validate([
+ 'note' => 'nullable|string',
+ 'items' => 'required|array',
+ 'items.*.id' => 'nullable|exists:course_request_items,id',
+ 'items.*.course_id' => 'required|exists:courses,id',
+ 'items.*.user_id' => 'nullable|exists:users,id',
+ 'items.*.group_id' => 'nullable|exists:groups,id',
+ 'items.*.start_date' => 'required|date',
+ 'items.*.end_date' => 'nullable|date|after:items.*.start_date',
+ ]);
+
+ DB::beginTransaction();
+
+ try {
+ $courseRequest->update(['note' => $validated['note'] ?? null]);
+
+ // Обновляем элементы
+ foreach ($validated['items'] as $itemData) {
+ if (!empty($itemData['id'])) {
+ // Обновляем существующий
+ $item = CourseRequestItem::find($itemData['id']);
+ $item->update([
+ 'course_id' => $itemData['course_id'],
+ 'user_id' => $itemData['user_id'] ?? null,
+ 'group_id' => $itemData['group_id'] ?? null,
+ 'start_date' => $itemData['start_date'],
+ 'end_date' => $itemData['end_date'] ?? null,
+ ]);
+ } else {
+ // Создаём новый
+ CourseRequestItem::create([
+ 'course_request_id' => $courseRequest->id,
+ 'course_id' => $itemData['course_id'],
+ 'user_id' => $itemData['user_id'] ?? null,
+ 'group_id' => $itemData['group_id'] ?? null,
+ 'start_date' => $itemData['start_date'],
+ 'end_date' => $itemData['end_date'] ?? null,
+ ]);
+ }
+ }
+
+ DB::commit();
+
+ return redirect()->route('admin.course-requests.show', $courseRequest)
+ ->with('success', 'Заявка обновлена.');
+
+ } catch (\Exception $e) {
+ DB::rollBack();
+ return back()->withErrors(['error' => 'Ошибка при обновлении: ' . $e->getMessage()])->withInput();
+ }
+ }
+
+ public function destroy(CourseRequest $courseRequest)
+ {
+ Gate::authorize('delete', $courseRequest);
+
+ if ($courseRequest->isApproved()) {
+ return back()->with('error', 'Нельзя удалить одобренную заявку.');
+ }
+
+ $courseRequest->delete();
+
+ return redirect()->route('admin.course-requests.index')
+ ->with('success', 'Заявка удалена.');
+ }
+
+ public function approve(CourseRequest $courseRequest, Request $request)
+ {
+ Gate::authorize('approve', $courseRequest);
+
+ if (!$courseRequest->isPending()) {
+ return back()->with('error', 'Можно одобрить только заявку в статусе ожидания.');
+ }
+
+ DB::beginTransaction();
+
+ try {
+ $courseRequest->approve(auth()->user());
+ $this->createAssignments($courseRequest);
+
+ DB::commit();
+
+ return back()->with('success', 'Заявка одобрена. Назначения созданы.');
+
+ } catch (\Exception $e) {
+ DB::rollBack();
+ return back()->withErrors(['error' => 'Ошибка при одобрении: ' . $e->getMessage()]);
+ }
+ }
+
+ public function reject(CourseRequest $courseRequest, Request $request)
+ {
+ Gate::authorize('reject', $courseRequest);
+
+ if (!$courseRequest->isPending()) {
+ return back()->with('error', 'Можно отклонить только заявку в статусе ожидания.');
+ }
+
+ $validated = $request->validate([
+ 'reject_reason' => 'nullable|string',
+ ]);
+
+ $courseRequest->reject(auth()->user());
+
+ return back()->with('success', 'Заявка отклонена.');
+ }
+
+ /**
+ * Создаёт назначения из одобренной заявки
+ */
+ private function createAssignments(CourseRequest $courseRequest): void
+ {
+ foreach ($courseRequest->items as $item) {
+ $type = $item->type;
+
+ CourseAssignment::create([
+ 'course_id' => $item->course_id,
+ 'organization_id' => $type === 'organization' ? $courseRequest->organization_id : null,
+ 'group_id' => $item->group_id,
+ 'user_id' => $item->user_id,
+ 'type' => $type,
+ 'start_date' => $item->start_date,
+ 'end_date' => $item->end_date,
+ 'note' => $courseRequest->note,
+ 'created_by' => $courseRequest->approved_by,
+ 'is_active' => true,
+ ]);
+ }
+ }
+}
diff --git a/app/Models/CourseRequest.php b/app/Models/CourseRequest.php
index 1f85efe..6a680be 100755
--- a/app/Models/CourseRequest.php
+++ b/app/Models/CourseRequest.php
@@ -14,10 +14,10 @@ class CourseRequest extends Model
protected $fillable = [
'organization_id',
'requested_by_user_id',
- 'approved_by_user_id',
'status',
- 'comment',
+ 'approved_by',
'approved_at',
+ 'note',
];
protected $casts = [
@@ -36,11 +36,44 @@ class CourseRequest extends Model
public function approvedBy(): BelongsTo
{
- return $this->belongsTo(User::class, 'approved_by_user_id');
+ return $this->belongsTo(User::class, 'approved_by');
}
public function items(): HasMany
{
return $this->hasMany(CourseRequestItem::class);
}
+
+ public function isPending(): bool
+ {
+ return $this->status === 'pending';
+ }
+
+ public function isApproved(): bool
+ {
+ return $this->status === 'approved';
+ }
+
+ public function isRejected(): bool
+ {
+ return $this->status === 'rejected';
+ }
+
+ public function approve(User $approvedBy): void
+ {
+ $this->update([
+ 'status' => 'approved',
+ 'approved_by' => $approvedBy->id,
+ 'approved_at' => now(),
+ ]);
+ }
+
+ public function reject(User $approvedBy): void
+ {
+ $this->update([
+ 'status' => 'rejected',
+ 'approved_by' => $approvedBy->id,
+ 'approved_at' => now(),
+ ]);
+ }
}
diff --git a/app/Models/CourseRequestItem.php b/app/Models/CourseRequestItem.php
index cc4a013..5a23612 100755
--- a/app/Models/CourseRequestItem.php
+++ b/app/Models/CourseRequestItem.php
@@ -11,12 +11,20 @@ class CourseRequestItem extends Model
use HasFactory;
protected $fillable = [
- 'request_id',
+ 'course_request_id',
'course_id',
'user_id',
+ 'group_id',
+ 'start_date',
+ 'end_date',
];
- public function request(): BelongsTo
+ protected $casts = [
+ 'start_date' => 'date',
+ 'end_date' => 'date',
+ ];
+
+ public function courseRequest(): BelongsTo
{
return $this->belongsTo(CourseRequest::class);
}
@@ -30,4 +38,19 @@ class CourseRequestItem extends Model
{
return $this->belongsTo(User::class);
}
+
+ public function group(): BelongsTo
+ {
+ return $this->belongsTo(Group::class);
+ }
+
+ public function getTypeAttribute(): string
+ {
+ if ($this->user_id) {
+ return 'individual';
+ } elseif ($this->group_id) {
+ return 'group';
+ }
+ return 'organization';
+ }
}
diff --git a/app/Policies/CourseRequestPolicy.php b/app/Policies/CourseRequestPolicy.php
new file mode 100755
index 0000000..0eb23d8
--- /dev/null
+++ b/app/Policies/CourseRequestPolicy.php
@@ -0,0 +1,44 @@
+hasRole(['Administrator', 'Manager', 'Curator']);
+ }
+
+ public function view(User $user, CourseRequest $courseRequest): bool
+ {
+ return $user->hasRole(['Administrator', 'Manager', 'Curator']);
+ }
+
+ public function create(User $user): bool
+ {
+ return $user->hasRole(['Administrator', 'Manager', 'Curator']);
+ }
+
+ public function update(User $user, CourseRequest $courseRequest): bool
+ {
+ return $user->hasRole(['Administrator', 'Manager', 'Curator']);
+ }
+
+ public function delete(User $user, CourseRequest $courseRequest): bool
+ {
+ return $user->hasRole(['Administrator', 'Manager']);
+ }
+
+ public function approve(User $user, CourseRequest $courseRequest): bool
+ {
+ return $user->hasRole(['Administrator', 'Manager', 'Curator']);
+ }
+
+ public function reject(User $user, CourseRequest $courseRequest): bool
+ {
+ return $user->hasRole(['Administrator', 'Manager', 'Curator']);
+ }
+}
diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php
index ee17e3d..685f1d8 100755
--- a/app/Providers/AuthServiceProvider.php
+++ b/app/Providers/AuthServiceProvider.php
@@ -23,6 +23,7 @@ class AuthServiceProvider extends ServiceProvider
CourseCategory::class => CourseCategoryPolicy::class,
Course::class => CoursePolicy::class,
CourseAssignment::class => CourseAssignmentPolicy::class,
+ CourseRequest::class => CourseRequestPolicy::class,
Test::class => TestPolicy::class,
Question::class => QuestionPolicy::class,
Organization::class => OrganizationPolicy::class,
diff --git a/database/migrations/2026_04_01_074209_create_course_requests_tables.php b/database/migrations/2026_04_01_074209_create_course_requests_tables.php
new file mode 100755
index 0000000..5047f8b
--- /dev/null
+++ b/database/migrations/2026_04_01_074209_create_course_requests_tables.php
@@ -0,0 +1,27 @@
+id();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('course_requests_tables');
+ }
+};
diff --git a/resources/views/admin/course-requests/index.blade.php b/resources/views/admin/course-requests/index.blade.php
new file mode 100644
index 0000000..dfcb689
--- /dev/null
+++ b/resources/views/admin/course-requests/index.blade.php
@@ -0,0 +1,104 @@
+@extends('layouts.app')
+@section('title', 'Заявки на курсы')
+@section('content')
+
+
+
+
+
+
Заявки на курсы
+
Создать заявку
+
+ @if(session('success')){{ session('success') }}
@endif
+ @if(session('error')){{ session('error') }}
@endif
+
+
+
+
+
+
+
+
+
+ | Организация |
+ Статус |
+ Курсов |
+ Создана |
+ Кем |
+ Действия |
+
+
+
+ @forelse($requests as $request)
+
+ | {{ $request->organization->name }} |
+
+ @if($request->isPending())
+ Ожидает
+ @elseif($request->isApproved())
+ Одобрена
+ @else
+ Отклонена
+ @endif
+ |
+ {{ $request->items->count() }} |
+ {{ $request->created_at->format('d.m.Y H:i') }} |
+ {{ $request->requestedBy->name }} |
+
+
+
+ @if($request->isPending())
+
+
+ @endif
+
+ |
+
+ @empty
+
+ |
+
+ Заявок пока нет
+ |
+
+ @endforelse
+
+
+
+
+
+ {{ $requests->links() }}
+
+
+
+@endsection
diff --git a/routes/web.php b/routes/web.php
index b2929b4..7d2f289 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -11,6 +11,7 @@ use App\Http\Controllers\Admin\TestController;
use App\Http\Controllers\Admin\QuestionController;
use App\Http\Controllers\Admin\CourseAssignmentController;
use App\Http\Controllers\Admin\GroupUserController;
+use App\Http\Controllers\Admin\CourseRequestController;
use App\Http\Controllers\Api\OrganizationSearchController;
use App\Http\Controllers\Api\GroupSearchController;
use App\Http\Controllers\Api\UserSearchController;
@@ -53,6 +54,9 @@ Route::middleware('auth')->group(function () {
Route::delete('/organizations/{organization}/users/{user}/remove', [OrganizationController::class, 'removeUser'])->name('organizations.users.remove');
Route::post('/organizations/{organization}/groups/add', [OrganizationController::class, 'addGroup'])->name('organizations.groups.add');
Route::delete('/organizations/{organization}/groups/{group}/remove', [OrganizationController::class, 'removeGroup'])->name('organizations.groups.remove');
+ Route::resource('course-requests', CourseRequestController::class);
+ Route::post('/course-requests/{courseRequest}/approve', [CourseRequestController::class, 'approve'])->name('course-requests.approve');
+ Route::post('/course-requests/{courseRequest}/reject', [CourseRequestController::class, 'reject'])->name('course-requests.reject');
Route::resource('users', UserController::class);
Route::resource('course-categories', CourseCategoryController::class);
Route::resource('courses', CourseController::class);