Feat: UI/UX групп как в назначениях
✅ AJAX добавление/удаление пользователей ✅ Modal редактирования группы ✅ Список пользователей с кнопками ✅ Счётчик обновляется без перезагрузки Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
ecdb8d10cb
commit
7fa8fd9a64
|
|
@ -78,7 +78,22 @@ class GroupUserController extends Controller
|
||||||
Gate::authorize('view', $group);
|
Gate::authorize('view', $group);
|
||||||
|
|
||||||
$group->load(['organization', 'users']);
|
$group->load(['organization', 'users']);
|
||||||
$users = User::where('organization_id', $group->organization_id)->get();
|
|
||||||
|
// Получаем доступных пользователей для этой группы
|
||||||
|
if ($group->organization_id) {
|
||||||
|
// Группа организации — только пользователи этой организации
|
||||||
|
$users = User::where('organization_id', $group->organization_id)
|
||||||
|
->whereDoesntHave('groups', function($q) use ($group) {
|
||||||
|
$q->where('groups.id', $group->id);
|
||||||
|
})
|
||||||
|
->get();
|
||||||
|
} else {
|
||||||
|
// Общая группа — все пользователи которые ещё не в группе
|
||||||
|
$users = User::whereDoesntHave('groups', function($q) use ($group) {
|
||||||
|
$q->where('groups.id', $group->id);
|
||||||
|
})
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
return view('admin.groups.show', compact('group', 'users'));
|
return view('admin.groups.show', compact('group', 'users'));
|
||||||
}
|
}
|
||||||
|
|
@ -145,18 +160,33 @@ class GroupUserController extends Controller
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'group_id' => 'required|exists:groups,id',
|
'group_id' => 'required|exists:groups,id',
|
||||||
|
'user_ids' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$group = Group::findOrFail($validated['group_id']);
|
$group = Group::findOrFail($validated['group_id']);
|
||||||
|
|
||||||
// Проверка доступа
|
// Если переданы user_ids (из tags-input)
|
||||||
if ($group->organization_id && $user->organization_id !== $group->organization_id) {
|
if (!empty($validated['user_ids'])) {
|
||||||
return back()->with('error', 'Нельзя добавить пользователя в группу другой организации.');
|
$userIds = array_map('intval', array_filter(explode(',', $validated['user_ids'])));
|
||||||
|
|
||||||
|
foreach ($userIds as $userId) {
|
||||||
|
$user = User::find($userId);
|
||||||
|
if (!$user) continue;
|
||||||
|
|
||||||
|
// Проверка доступа
|
||||||
|
if ($group->organization_id && $user->organization_id !== $group->organization_id) {
|
||||||
|
continue; // Пропускаем пользователей из других организаций
|
||||||
|
}
|
||||||
|
|
||||||
|
$group->users()->attach($userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$group->users()->attach($user->id);
|
if ($request->ajax()) {
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
return back()->with('success', 'Пользователь добавлен в группу.');
|
return back()->with('success', 'Пользователи добавлены в группу.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -168,6 +198,45 @@ class GroupUserController extends Controller
|
||||||
|
|
||||||
$group->users()->detach($user->id);
|
$group->users()->detach($user->id);
|
||||||
|
|
||||||
|
// Для AJAX запросов
|
||||||
|
if (request()->ajax()) {
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
return back()->with('success', 'Пользователь удалён из группы.');
|
return back()->with('success', 'Пользователь удалён из группы.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Редактирование группы (modal)
|
||||||
|
*/
|
||||||
|
public function edit(Group $group)
|
||||||
|
{
|
||||||
|
Gate::authorize('update', $group);
|
||||||
|
|
||||||
|
return redirect()->route('admin.groups.show', $group)->with('edit', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновление группы
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Group $group)
|
||||||
|
{
|
||||||
|
Gate::authorize('update', $group);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$group->update($validated);
|
||||||
|
|
||||||
|
// Для AJAX запросов
|
||||||
|
if ($request->ajax()) {
|
||||||
|
return response()->json(['success' => true, 'group' => $group]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('admin.groups.show', $group)
|
||||||
|
->with('success', 'Группа успешно обновлена.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,54 +8,64 @@
|
||||||
<div class="d-flex justify-content-between align-items-center pt-3 pb-2 mb-3 border-bottom">
|
<div class="d-flex justify-content-between align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<h1 class="h2">{{ $group->name }}</h1>
|
<h1 class="h2">{{ $group->name }}</h1>
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ route('admin.groups.edit', $group) }}" class="btn btn-warning btn-sm me-2">Редактировать</a>
|
<button class="btn btn-warning btn-sm me-2" data-bs-toggle="modal" data-bs-target="#editGroupModal">
|
||||||
|
<i class="bi bi-pencil"></i> Редактировать
|
||||||
|
</button>
|
||||||
<a href="{{ route('admin.groups.index') }}" class="btn btn-secondary btn-sm">Назад</a>
|
<a href="{{ route('admin.groups.index') }}" class="btn btn-secondary btn-sm">Назад</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row mb-4">
|
||||||
<div class="col-md-4 mb-4">
|
<div class="col-md-6">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header bg-primary text-white"><h5 class="mb-0">Информация</h5></div>
|
<div class="card-header bg-primary text-white"><h5 class="mb-0">Информация</h5></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-sm">
|
<div><strong>Организация:</strong> {{ $group->organization?->name ?? 'Общая группа' }}</div>
|
||||||
<tr><th>Организация:</th><td>{{ $group->organization?->name ?? '—' }}</td></tr>
|
<div><strong>Описание:</strong> {{ $group->description ?? '—' }}</div>
|
||||||
<tr><th>Описание:</th><td>{{ $group->description ?? '—' }}</td></tr>
|
<div><strong>Статус:</strong>
|
||||||
<tr><th>Статус:</th><td>@if($group->is_active)<span class="badge bg-success">Активна</span>@else<span class="badge bg-secondary">Не активна</span>@endif</td></tr>
|
@if($group->is_active)
|
||||||
<tr><th>Создана:</th><td>{{ $group->created_at->format('d.m.Y') }}</td></tr>
|
<span class="badge bg-success">Активна</span>
|
||||||
</table>
|
@else
|
||||||
|
<span class="badge bg-secondary">Не активна</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div><strong>Создана:</strong> {{ $group->created_at->format('d.m.Y') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
<div class="col-md-8 mb-4">
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header bg-success text-white"><h5 class="mb-0">Пользователи в группе ({{ $group->users->count() }})</h5></div>
|
<div class="card-header bg-success text-white"><h5 class="mb-0">Статистика</h5></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@if($group->users->count() > 0)
|
<div class="display-6 text-success" id="users-count">{{ $group->users->count() }}</div>
|
||||||
<div class="table-responsive">
|
<div class="text-muted"><i class="bi bi-people"></i> Пользователей в группе</div>
|
||||||
<table class="table table-sm">
|
</div>
|
||||||
<thead>
|
</div>
|
||||||
<tr>
|
</div>
|
||||||
<th>Имя</th>
|
</div>
|
||||||
<th>Email</th>
|
|
||||||
<th>Должность</th>
|
<div class="row">
|
||||||
</tr>
|
<div class="col-md-12 mb-4">
|
||||||
</thead>
|
<div class="card shadow-sm h-100">
|
||||||
<tbody>
|
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
||||||
@foreach($group->users as $user)
|
<h5 class="mb-0"><i class="bi bi-people"></i> Пользователи</h5>
|
||||||
<tr>
|
<button class="btn btn-sm btn-light" data-bs-toggle="modal" data-bs-target="#addUserModal"><i class="bi bi-plus-lg"></i></button>
|
||||||
<td>{{ $user->name }}</td>
|
</div>
|
||||||
<td>{{ $user->email }}</td>
|
<div class="card-body p-0">
|
||||||
<td>{{ $user->getRoleNames()->first() ?? '—' }}</td>
|
<div id="users-list">
|
||||||
</tr>
|
@if($group->users->count() > 0)
|
||||||
@endforeach
|
<ul class="list-group list-group-flush" id="users-ul">
|
||||||
</tbody>
|
@foreach($group->users as $user)
|
||||||
</table>
|
<li class="list-group-item d-flex justify-content-between align-items-center" id="user-li-{{ $user->id }}">
|
||||||
|
<a href="{{ route('admin.users.show', $user) }}">{{ $user->name }} <small class="text-muted">({{ $user->email }})</small></a>
|
||||||
|
<button onclick="removeUser({{ $user->id }})" class="btn btn-sm btn-outline-danger"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@else
|
||||||
|
<p class="text-muted text-center py-4" id="users-empty">Нет пользователей</p>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@else
|
|
||||||
<p class="text-muted mb-0">В этой группе пока нет пользователей</p>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -63,4 +73,148 @@
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal добавления пользователя -->
|
||||||
|
<div class="modal fade" id="addUserModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="addUserForm">
|
||||||
|
@csrf
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Добавить пользователя</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<x-tags-input name="user_ids" url="{{ route('api.users.search') }}" placeholder="Начните вводить имя..." badge_color="success" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
|
<button type="submit" class="btn btn-success">Добавить</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal редактирования группы -->
|
||||||
|
<div class="modal fade" id="editGroupModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="editGroupForm">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Редактировать группу</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Название *</label>
|
||||||
|
<input type="text" name="name" class="form-control" value="{{ $group->name }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Описание</label>
|
||||||
|
<textarea name="description" class="form-control" rows="3">{{ $group->description }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input type="checkbox" name="is_active" value="1" class="form-check-input" {{ $group->is_active ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label">Активна</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
|
<button type="submit" class="btn btn-warning">Сохранить</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
// AJAX добавление пользователя
|
||||||
|
document.getElementById('addUserForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
fetch('/admin/groups/{{ $group->id }}/users/0/add', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'Ошибка добавления');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
// AJAX удаление пользователя
|
||||||
|
function removeUser(userId) {
|
||||||
|
if (!confirm('Удалить пользователя из группы?')) return;
|
||||||
|
|
||||||
|
fetch('/admin/groups/{{ $group->id }}/users/' + userId + '/remove', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const li = document.getElementById('user-li-' + userId);
|
||||||
|
if (li) li.remove();
|
||||||
|
|
||||||
|
const countEl = document.getElementById('users-count');
|
||||||
|
if (countEl) countEl.textContent = parseInt(countEl.textContent) - 1;
|
||||||
|
|
||||||
|
const ul = document.getElementById('users-ul');
|
||||||
|
const empty = document.getElementById('users-empty');
|
||||||
|
if (ul && ul.children.length === 0 && empty) {
|
||||||
|
empty.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
// AJAX редактирование группы
|
||||||
|
document.getElementById('editGroupForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
fetch('{{ route('groups.update', $group) }}', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Автооткрытие modal редактирования
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
@if(session('edit'))
|
||||||
|
const editModal = new bootstrap.Modal(document.getElementById('editGroupModal'));
|
||||||
|
editModal.show();
|
||||||
|
@endif
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
@endsection
|
@endsection
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,9 @@ Route::middleware('auth')->group(function () {
|
||||||
Route::resource('tests.questions', QuestionController::class);
|
Route::resource('tests.questions', QuestionController::class);
|
||||||
Route::resource('course-assignments', CourseAssignmentController::class)->except(['show', 'edit', 'update']);
|
Route::resource('course-assignments', CourseAssignmentController::class)->except(['show', 'edit', 'update']);
|
||||||
Route::get('/course-assignments/{course}', CourseAssignmentController::class . '@show')->name('course-assignments.show');
|
Route::get('/course-assignments/{course}', CourseAssignmentController::class . '@show')->name('course-assignments.show');
|
||||||
Route::resource('groups', GroupUserController::class);
|
Route::resource('groups', GroupUserController::class)->except(['edit', 'update']);
|
||||||
|
Route::get('/groups/{group}/edit', [GroupUserController::class, 'edit'])->name('groups.edit');
|
||||||
|
Route::put('/groups/{group}', [GroupUserController::class, 'update'])->name('groups.update');
|
||||||
Route::post('/users/{user}/groups/add', [GroupUserController::class, 'addUser'])->name('groups.users.add');
|
Route::post('/users/{user}/groups/add', [GroupUserController::class, 'addUser'])->name('groups.users.add');
|
||||||
Route::delete('/groups/{group}/users/{user}/remove', [GroupUserController::class, 'removeUser'])->name('groups.users.remove');
|
Route::delete('/groups/{group}/users/{user}/remove', [GroupUserController::class, 'removeUser'])->name('groups.users.remove');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue