Usuwanie uzytkownika

This commit is contained in:
Tomasz Boruc 2026-08-28 00:02:56 +02:00
parent a680be3fd3
commit 97adda56d3
10 changed files with 272 additions and 24 deletions

View File

@ -12,11 +12,11 @@ class DashboardController extends Controller
*/
public function index(): View
{
$totalUsers = User::count();
$adminCount = User::role('admin')->count();
$managerCount = User::role('kierownik')->count();
$workerCount = User::role('pracownik')->count();
$recentUsers = User::latest()->take(5)->get();
$totalUsers = User::whereNull('deleted_at')->count();
$adminCount = User::role('admin')->whereNull('deleted_at')->count();
$managerCount = User::role('kierownik')->whereNull('deleted_at')->count();
$workerCount = User::role('pracownik')->whereNull('deleted_at')->count();
$recentUsers = User::whereNull('deleted_at')->latest()->take(5)->get();
return view('dashboard', compact(
'totalUsers',

View File

@ -58,7 +58,7 @@ class ProfileController extends Controller
Auth::logout();
$user->delete();
$user->forceDelete();
$request->session()->invalidate();
$request->session()->regenerateToken();

View File

@ -22,7 +22,7 @@ class UserController extends Controller
$direction = $request->query('direction') === 'desc' ? 'desc' : 'asc';
$search = trim($request->query('search'));
$query = User::with('roles');
$query = User::with('roles')->whereNull('deleted_at');
// Wyszukiwanie w imieniu, nazwisku i adresie email
if ($search) {
@ -46,6 +46,35 @@ class UserController extends Controller
return view('users.index', compact('users'));
}
public function deleted(Request $request)
{
$allowedSorts = ['name', 'email'];
$sort = in_array($request->query('sort'), $allowedSorts) ? $request->query('sort') : 'name';
$direction = $request->query('direction') === 'desc' ? 'desc' : 'asc';
$search = trim($request->query('search'));
$query = User::with('roles')->onlyTrashed();
if ($search) {
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('surname', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
}
if ($sort === 'name') {
$query->orderBy('name', $direction)->orderBy('surname', $direction);
} else {
$query->orderBy($sort, $direction);
}
$users = $query->paginate(10)->withQueryString();
return view('users.deleted', compact('users'));
}
/**
* Formularz dodawania nowego użytkownika.
* Wykorzystuje gotowy widok z Breeze (auth.register).
@ -129,6 +158,14 @@ class UserController extends Controller
$user->delete();
return redirect()->route('users.index')->with('status', 'Użytkownik został usunięty.');
return redirect()->route('users.index')->with('status', 'Użytkownik został oznaczony jako usunięty.');
}
public function restore($userId)
{
$user = User::withTrashed()->findOrFail($userId);
$user->restore();
return redirect()->route('users.deleted')->with('status', 'Użytkownik został przywrócony.');
}
}

View File

@ -5,6 +5,7 @@ namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasRoles; // 1. Import traitu Spatie
@ -12,7 +13,7 @@ use Spatie\Permission\Traits\HasRoles; // 1. Import traitu Spatie
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable, HasRoles; // 2. Dodanie HasRoles
use HasFactory, Notifiable, HasRoles, SoftDeletes; // 2. Dodanie HasRoles i soft delete
/**
* The attributes that are mass assignable.

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
};

View File

@ -0,0 +1,105 @@
<x-app-layout>
<x-slot name="header">
<div class="flex items-center justify-between">
<h2 class="font-semibold text-xl text-gray-800 dark:text-white leading-tight">
{{ __('Użytkownicy usunięci') }}
</h2>
<a href="{{ route('users.index') }}"
class="inline-flex items-center px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-md shadow-sm transition ease-in-out duration-150 gap-2">
<span>{{ __('Powrót do użytkowników') }}</span>
</a>
</div>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg border border-gray-200 dark:border-gray-700/50">
<div class="p-6 text-gray-900 dark:text-white">
<div class="mb-6 flex items-center justify-between gap-4">
<form method="GET" action="{{ route('users.deleted') }}" class="flex items-center gap-2 w-full max-w-xs">
@if(request('sort'))
<input type="hidden" name="sort" value="{{ request('sort') }}">
@endif
@if(request('direction'))
<input type="hidden" name="direction" value="{{ request('direction') }}">
@endif
<div class="w-full">
<x-text-input
type="text"
name="search"
:value="request('search')"
placeholder="Szukaj usuniętego użytkownika..."
class="block w-full px-3 py-2 text-sm"
/>
</div>
<x-primary-button type="submit">
Szukaj
</x-primary-button>
@if(request('search'))
<a href="{{ route('users.deleted', request()->only(['sort', 'direction'])) }}"
class="inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition ease-in-out duration-150 whitespace-nowrap">
Wyczyść
</a>
@endif
</form>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse" style="table-layout: fixed;">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-300">
<th scope="col" class="py-3.5 px-4" style="text-align: center !important; width: 25%;">Imię i nazwisko</th>
<th scope="col" class="py-3.5 px-4" style="text-align: center !important; width: 35%;">Adres e-mail</th>
<th scope="col" class="py-3.5 px-4" style="text-align: center !important; width: 20%;">Usunięty</th>
<th scope="col" class="py-3.5 px-4" style="text-align: center !important; width: 20%;">Akcje</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700/60 text-sm">
@forelse($users as $user)
<tr class="transition-colors duration-150">
<td class="py-4 px-4 font-medium text-gray-900 dark:text-white align-middle" style="text-align: center !important;">
{{ $user->name }} {{ $user->surname }}
</td>
<td class="py-4 px-4 text-gray-600 dark:text-gray-300 align-middle" style="text-align: center !important;">
{{ $user->email }}
</td>
<td class="py-4 px-4 text-gray-600 dark:text-gray-300 align-middle" style="text-align: center !important;">
{{ $user->deleted_at?->format('d.m.Y H:i') ?? '-' }}
</td>
<td class="py-4 px-4 align-middle" style="text-align: center !important;">
<form method="POST" action="{{ route('users.restore', ['user' => $user->id]) }}" onsubmit="return confirm('Czy na pewno chcesz przywrócić tego użytkownika?');">
@csrf
<button type="submit" class="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 font-medium text-sm transition">
Przywróć
</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="py-8 text-gray-500 dark:text-gray-400" style="text-align: center !important;">
Brak usuniętych użytkowników.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($users->hasPages())
<div class="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
{{ $users->links() }}
</div>
@endif
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -60,11 +60,23 @@
</div>
<!-- Przyciski -->
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Zapisz') }}</x-primary-button>
<a href="{{ route('users.index') }}" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition">
{{ __('Anuluj') }}
</a>
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Zapisz') }}</x-primary-button>
<a href="{{ route('users.index') }}" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition">
{{ __('Anuluj') }}
</a>
</div>
@if(auth()->id() !== $user->id)
<form method="POST" action="{{ route('users.destroy', $user) }}" onsubmit="return confirm('Czy na pewno chcesz usunąć tego użytkownika?');">
@csrf
@method('DELETE')
<button type="submit" class="inline-flex items-center px-4 py-2 bg-red-600 hover:bg-red-500 text-white text-sm font-medium rounded-md shadow-sm transition">
{{ __('Usuń użytkownika') }}
</button>
</form>
@endif
</div>
</form>
</section>

View File

@ -6,13 +6,20 @@
</h2>
@hasanyrole('admin|kierownik')
<a href="{{ route('users.create') }}"
class="inline-flex items-center px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-md shadow-sm transition ease-in-out duration-150 gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
<span>{{ __('Dodaj użytkownika') }}</span>
</a>
<div class="flex items-center gap-3">
<a href="{{ route('users.deleted') }}"
class="inline-flex items-center px-4 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-white text-sm font-medium rounded-md shadow-sm transition ease-in-out duration-150 gap-2">
<span>{{ __('Użytkownicy usunięci') }}</span>
</a>
<a href="{{ route('users.create') }}"
class="inline-flex items-center px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-md shadow-sm transition ease-in-out duration-150 gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
<span>{{ __('Dodaj użytkownika') }}</span>
</a>
</div>
@endhasanyrole
</div>
</x-slot>
@ -141,9 +148,21 @@
</td>
<td class="py-4 px-4 align-middle" style="text-align: center !important;">
<a href="{{ route('users.edit', $user) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300 font-medium text-sm transition">
Edytuj
</a>
<div class="flex items-center justify-center gap-3">
<a href="{{ route('users.edit', $user) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300 font-medium text-sm transition">
Edytuj
</a>
@if(auth()->id() !== $user->id)
<form method="POST" action="{{ route('users.destroy', $user) }}" onsubmit="return confirm('Czy na pewno chcesz usunąć tego użytkownika?');">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300 font-medium text-sm transition">
Usuń
</button>
</form>
@endif
</div>
</td>
</tr>

View File

@ -25,11 +25,13 @@ Route::middleware(['auth', 'verified'])->group(function () {
// Zarządzanie użytkownikami (tylko dla posiadaczy uprawnienia users_access)
Route::middleware(['can:users_access'])->group(function () {
Route::get('/users', [UserController::class, 'index'])->name('users.index');
Route::get('/users/deleted', [UserController::class, 'deleted'])->name('users.deleted');
Route::get('/users/create', [UserController::class, 'create'])->name('users.create');
Route::post('/users', [UserController::class, 'store'])->name('users.store');
Route::get('/users/{user}/edit', [UserController::class, 'edit'])->name('users.edit');
Route::put('/users/{user}', [UserController::class, 'update'])->name('users.update');
Route::delete('/users/{user}', [UserController::class, 'destroy'])->name('users.destroy');
Route::post('/users/{user}/restore', [UserController::class, 'restore'])->name('users.restore');
});
// Moduł Raporty

View File

@ -0,0 +1,44 @@
<?php
use App\Models\User;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
test('user can be soft deleted and hidden from the list', function () {
Permission::firstOrCreate(['name' => 'users_access']);
$admin = User::factory()->create();
$adminRole = Role::firstOrCreate(['name' => 'admin']);
$admin->assignRole($adminRole);
$admin->givePermissionTo('users_access');
$user = User::factory()->create();
$response = $this
->actingAs($admin)
->delete('/users/' . $user->id);
$response->assertRedirect(route('users.index'));
$this->assertSoftDeleted($user);
$this->assertNotNull($user->fresh()->deleted_at);
});
test('deleted users can be listed and restored', function () {
Permission::firstOrCreate(['name' => 'users_access']);
$admin = User::factory()->create();
$adminRole = Role::firstOrCreate(['name' => 'admin']);
$admin->assignRole($adminRole);
$admin->givePermissionTo('users_access');
$user = User::factory()->create();
$user->delete();
$indexResponse = $this->actingAs($admin)->get('/users/deleted');
$indexResponse->assertOk();
$indexResponse->assertSee($user->email);
$restoreResponse = $this->actingAs($admin)->post('/users/' . $user->id . '/restore');
$restoreResponse->assertRedirect(route('users.deleted'));
$this->assertNull($user->fresh()->deleted_at);
});