diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php
index 2bf4310..8b751e6 100644
--- a/app/Http/Controllers/DashboardController.php
+++ b/app/Http/Controllers/DashboardController.php
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\View\View;
+use Spatie\Permission\Models\Role;
class DashboardController extends Controller
{
@@ -13,17 +14,37 @@ class DashboardController extends Controller
public function index(): View
{
$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();
+ $adminCount = Role::where('name', 'admin')->exists()
+ ? User::role('admin')->whereNull('deleted_at')->count()
+ : 0;
+ $managerCount = Role::where('name', 'kierownik')->exists()
+ ? User::role('kierownik')->whereNull('deleted_at')->count()
+ : 0;
+ $workerCount = Role::where('name', 'pracownik')->exists()
+ ? User::role('pracownik')->whereNull('deleted_at')->count()
+ : 0;
+ $recentUsers = User::with('advances')
+ ->whereNull('deleted_at')
+ ->get()
+ ->filter(fn (User $user) => (float) $user->advances->sum('amount') < 0)
+ ->sortByDesc('created_at')
+ ->take(5)
+ ->values();
+ $lowestAdvanceBalances = User::with('advances')
+ ->whereNull('deleted_at')
+ ->get()
+ ->filter(fn (User $user) => (float) $user->advances->sum('amount') < 0)
+ ->sortBy(fn (User $user) => (float) $user->advances->sum('amount'))
+ ->take(10)
+ ->values();
return view('dashboard', compact(
'totalUsers',
'adminCount',
'managerCount',
'workerCount',
- 'recentUsers'
+ 'recentUsers',
+ 'lowestAdvanceBalances'
));
}
}
diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index 476cafb..d239f67 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -115,6 +115,16 @@ class UserController extends Controller
/**
* Formularz edycji wybranego użytkownika.
*/
+ public function show(User $user)
+ {
+ $user->load(['roles', 'advances.type']);
+
+ return view('users.show', [
+ 'user' => $user,
+ 'balance' => $user->advanceBalance()->value('balance') ?? 0,
+ ]);
+ }
+
public function edit(User $user)
{
$roles = Role::all();
diff --git a/app/Models/Advance.php b/app/Models/Advance.php
index d6f2cbe..dfb5d7a 100644
--- a/app/Models/Advance.php
+++ b/app/Models/Advance.php
@@ -33,6 +33,28 @@ class Advance extends Model
$advance->amount = $typeName === 'Wpłata' ? $amount : -$amount;
});
+
+ static::saved(function (Advance $advance) {
+ static::refreshUserAdvanceBalance($advance->user_id);
+
+ if ($advance->wasChanged('user_id')) {
+ static::refreshUserAdvanceBalance($advance->getOriginal('user_id'));
+ }
+ });
+
+ static::deleted(function (Advance $advance) {
+ static::refreshUserAdvanceBalance($advance->user_id);
+ });
+ }
+
+ private static function refreshUserAdvanceBalance(int $userId): void
+ {
+ $balance = static::where('user_id', $userId)->sum('amount');
+
+ UserAdvanceBalance::updateOrCreate(
+ ['user_id' => $userId],
+ ['balance' => (float) $balance]
+ );
}
public function user(): BelongsTo
diff --git a/app/Models/User.php b/app/Models/User.php
index 6544e7f..1a05cb0 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\HasOne;
use Spatie\Permission\Traits\HasRoles; // 1. Import traitu Spatie
class User extends Authenticatable
@@ -59,4 +60,9 @@ class User extends Authenticatable
{
return $this->hasMany(Advance::class);
}
+
+ public function advanceBalance(): HasOne
+ {
+ return $this->hasOne(UserAdvanceBalance::class);
+ }
}
\ No newline at end of file
diff --git a/app/Models/UserAdvanceBalance.php b/app/Models/UserAdvanceBalance.php
new file mode 100644
index 0000000..7562957
--- /dev/null
+++ b/app/Models/UserAdvanceBalance.php
@@ -0,0 +1,29 @@
+ 'decimal:2',
+ ];
+ }
+
+ public function user(): BelongsTo
+ {
+ return $this->belongsTo(User::class);
+ }
+}
diff --git a/database/migrations/2026_08_29_000001_create_user_advance_balances_table.php b/database/migrations/2026_08_29_000001_create_user_advance_balances_table.php
new file mode 100644
index 0000000..065db2b
--- /dev/null
+++ b/database/migrations/2026_08_29_000001_create_user_advance_balances_table.php
@@ -0,0 +1,23 @@
+id();
+ $table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
+ $table->decimal('balance', 12, 2)->default(0);
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('user_advance_balances');
+ }
+};
diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php
index 09b6fa2..598b938 100644
--- a/database/seeders/UserSeeder.php
+++ b/database/seeders/UserSeeder.php
@@ -45,7 +45,7 @@ class UserSeeder extends Seeder
$pracownik->assignRole($pracownikRole);
// 2. Generowanie dodatkowych 30 losowych użytkowników z rolami (password = 'password')
- User::factory(30)->create()->each(function ($user) use ($pracownikRole, $kierownikRole) {
+ User::factory(5)->create()->each(function ($user) use ($pracownikRole, $kierownikRole) {
$role = rand(1, 100) <= 80 ? $pracownikRole : $kierownikRole;
$user->assignRole($role);
});
diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php
index c8f14dc..4ca0485 100644
--- a/resources/views/dashboard.blade.php
+++ b/resources/views/dashboard.blade.php
@@ -133,6 +133,44 @@
+ {{-- Najniższy bilans zaliczek --}}
+
+
+ Najniższy bilans zaliczek
+
+
+
+
+
+
+ | # |
+ Pracownik |
+ Bilans |
+
+
+
+ @forelse($lowestAdvanceBalances ?? [] as $index => $user)
+
+ | {{ $index + 1 }} |
+
+
+ {{ $user->name }} {{ $user->surname }}
+
+ |
+
+ {{ number_format((float) ($user->advanceBalance?->balance ?? 0), 2, ',', ' ') }} zł
+ |
+
+ @empty
+
+ | Brak danych o bilansach zaliczek. |
+
+ @endforelse
+
+
+
+
+
diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php
index 250cf45..c298bb6 100644
--- a/resources/views/users/index.blade.php
+++ b/resources/views/users/index.blade.php
@@ -128,7 +128,9 @@
|
- {{ $user->name }} {{ $user->surname }}
+
+ {{ $user->name }} {{ $user->surname }}
+
|
diff --git a/resources/views/users/show.blade.php b/resources/views/users/show.blade.php
new file mode 100644
index 0000000..3b77c89
--- /dev/null
+++ b/resources/views/users/show.blade.php
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+ Pracownik
+ {{ $user->name }} {{ $user->surname }}
+
+
+
+ Bilans zaliczek
+
+ {{ number_format((float) $balance, 2, ',', ' ') }} zł
+
+
+
+
+
+
+
+
+ Dane osobowe
+
+
+
+ - Imię
+ - {{ $user->name }}
+
+
+ - Nazwisko
+ - {{ $user->surname }}
+
+
+ - E-mail
+ - {{ $user->email }}
+
+
+ - Role
+ -
+ @forelse($user->roles as $role)
+
+ {{ $role->name }}
+
+ @empty
+ Brak ról
+ @endforelse
+
+
+
+
+
+
+ Podsumowanie finansowe
+
+ @if($user->advances->isEmpty())
+ Brak zaliczek dla tego pracownika.
+ @else
+
+
+
+
+ | Data |
+ Typ |
+ Kwota |
+
+
+
+ @foreach($user->advances->sortByDesc('date') as $advance)
+
+ | {{ $advance->date->format('d.m.Y') }} |
+ {{ $advance->type?->name ?? '—' }} |
+
+ {{ number_format((float) $advance->amount, 2, ',', ' ') }} zł
+ |
+
+ @endforeach
+
+
+
+ @endif
+
+
+
+
+
diff --git a/routes/web.php b/routes/web.php
index be067b2..5489501 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -29,6 +29,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
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}', [UserController::class, 'show'])->name('users.show');
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');
diff --git a/tests/Feature/AdvanceTest.php b/tests/Feature/AdvanceTest.php
index 2dc4543..10224c4 100644
--- a/tests/Feature/AdvanceTest.php
+++ b/tests/Feature/AdvanceTest.php
@@ -138,3 +138,133 @@ test('manager can filter advances by text type and date range', function () {
$response->assertSee('100,00');
$response->assertDontSee('200,00');
});
+
+test('manager can open employee profile and see current advance balance', function () {
+ Permission::firstOrCreate(['name' => 'users_access']);
+
+ $manager = User::factory()->create();
+ $managerRole = Role::firstOrCreate(['name' => 'kierownik']);
+ $managerRole->givePermissionTo(['zaliczki_access', 'users_access']);
+ $manager->assignRole($managerRole);
+
+ $employee = User::factory()->create(['name' => 'Jan', 'surname' => 'Kowalski']);
+ $fuel = AdvanceType::create(['name' => 'Paliwo']);
+ $deposit = AdvanceType::create(['name' => 'Wpłata']);
+
+ Advance::create(['date' => '2026-08-10', 'user_id' => $employee->id, 'amount' => 120, 'advance_type_id' => $fuel->id]);
+ Advance::create(['date' => '2026-08-11', 'user_id' => $employee->id, 'amount' => 500, 'advance_type_id' => $deposit->id]);
+
+ $response = $this->actingAs($manager)->get(route('users.show', $employee));
+
+ $response->assertOk();
+ $response->assertSee('Jan Kowalski');
+ $response->assertSee('380,00');
+});
+
+test('advance balance is recalculated after create update and delete', function () {
+ $employee = userWithAdvanceAccess();
+ $fuel = AdvanceType::create(['name' => 'Paliwo']);
+
+ $advance = Advance::create(['date' => '2026-08-10', 'user_id' => $employee->id, 'amount' => 120, 'advance_type_id' => $fuel->id]);
+
+ $this->assertDatabaseHas('user_advance_balances', [
+ 'user_id' => $employee->id,
+ 'balance' => -120,
+ ]);
+
+ $advance->update(['amount' => 300]);
+
+ $this->assertDatabaseHas('user_advance_balances', [
+ 'user_id' => $employee->id,
+ 'balance' => -300,
+ ]);
+
+ $advance->delete();
+
+ $this->assertDatabaseHas('user_advance_balances', [
+ 'user_id' => $employee->id,
+ 'balance' => 0,
+ ]);
+});
+
+test('dashboard shows the lowest advance balances for employees', function () {
+ $manager = userWithAdvanceAccess('kierownik');
+ $fuel = AdvanceType::create(['name' => 'Paliwo']);
+ Role::firstOrCreate(['name' => 'pracownik']);
+
+ $worstEmployee = User::factory()->create(['name' => 'Anna', 'surname' => 'Najgorsza']);
+ $worstEmployee->assignRole('pracownik');
+ $betterEmployee = User::factory()->create(['name' => 'Piotr', 'surname' => 'Lepszy']);
+ $betterEmployee->assignRole('pracownik');
+
+ Advance::create(['date' => '2026-08-10', 'user_id' => $worstEmployee->id, 'amount' => 200, 'advance_type_id' => $fuel->id]);
+ Advance::create(['date' => '2026-08-11', 'user_id' => $betterEmployee->id, 'amount' => 50, 'advance_type_id' => $fuel->id]);
+
+ $response = $this->actingAs($manager)->get(route('dashboard'));
+
+ $response->assertOk();
+ $response->assertSee('Najniższy bilans zaliczek');
+ $response->assertSee('Anna Najgorsza');
+ $response->assertSee('-200,00');
+});
+
+test('dashboard prioritizes negative balances over zero balances', function () {
+ $manager = userWithAdvanceAccess('kierownik');
+ $fuel = AdvanceType::create(['name' => 'Paliwo']);
+ Role::firstOrCreate(['name' => 'pracownik']);
+
+ $negativeEmployee = User::factory()->create(['name' => 'Kasia', 'surname' => 'Minus']);
+ $negativeEmployee->assignRole('pracownik');
+ $zeroEmployee = User::factory()->create(['name' => 'Marek', 'surname' => 'Zero']);
+ $zeroEmployee->assignRole('pracownik');
+
+ Advance::create(['date' => '2026-08-01', 'user_id' => $negativeEmployee->id, 'amount' => 100, 'advance_type_id' => $fuel->id]);
+ Advance::create(['date' => '2026-08-02', 'user_id' => $zeroEmployee->id, 'amount' => 0, 'advance_type_id' => $fuel->id]);
+
+ $response = $this->actingAs($manager)->get(route('dashboard'));
+
+ $response->assertOk();
+ $response->assertSeeInOrder(['Kasia Minus']);
+ $response->assertDontSee('Marek Zero');
+});
+
+test('dashboard hides employees with zero or positive balance from the negative ledger', function () {
+ $manager = userWithAdvanceAccess('kierownik');
+ $fuel = AdvanceType::create(['name' => 'Paliwo']);
+ $deposit = AdvanceType::create(['name' => 'Wpłata']);
+ Role::firstOrCreate(['name' => 'pracownik']);
+
+ $negativeEmployee = User::factory()->create(['name' => 'Kasia', 'surname' => 'Minus']);
+ $negativeEmployee->assignRole('pracownik');
+ $zeroEmployee = User::factory()->create(['name' => 'Marek', 'surname' => 'Zero']);
+ $zeroEmployee->assignRole('pracownik');
+ $positiveEmployee = User::factory()->create(['name' => 'Ola', 'surname' => 'Plus']);
+ $positiveEmployee->assignRole('pracownik');
+
+ Advance::create(['date' => '2026-08-01', 'user_id' => $negativeEmployee->id, 'amount' => 100, 'advance_type_id' => $fuel->id]);
+ Advance::create(['date' => '2026-08-02', 'user_id' => $zeroEmployee->id, 'amount' => 0, 'advance_type_id' => $fuel->id]);
+ Advance::create(['date' => '2026-08-03', 'user_id' => $positiveEmployee->id, 'amount' => 50, 'advance_type_id' => $deposit->id]);
+
+ $response = $this->actingAs($manager)->get(route('dashboard'));
+
+ $response->assertOk();
+ $response->assertSee('Kasia Minus');
+ $response->assertDontSee('Marek Zero');
+ $response->assertDontSee('Ola Plus');
+});
+
+test('dashboard shows negative balances for users regardless of role', function () {
+ $manager = userWithAdvanceAccess('kierownik');
+ $fuel = AdvanceType::create(['name' => 'Paliwo']);
+
+ $user = User::factory()->create(['name' => 'Zbyszek', 'surname' => 'Minus']);
+ $user->syncRoles([]);
+
+ Advance::create(['date' => '2026-08-10', 'user_id' => $user->id, 'amount' => 150, 'advance_type_id' => $fuel->id]);
+
+ $response = $this->actingAs($manager)->get(route('dashboard'));
+
+ $response->assertOk();
+ $response->assertSee('Zbyszek Minus');
+ $response->assertSee('-150,00');
+});
|