From b45325aac1d9318bc5fcbcaed67d850859334153 Mon Sep 17 00:00:00 2001 From: Tomasz Boruc Date: Sat, 29 Aug 2026 20:49:23 +0200 Subject: [PATCH] Bilans zaliczek --- app/Http/Controllers/DashboardController.php | 31 ++++- app/Http/Controllers/UserController.php | 10 ++ app/Models/Advance.php | 22 +++ app/Models/User.php | 6 + app/Models/UserAdvanceBalance.php | 29 ++++ ...001_create_user_advance_balances_table.php | 23 ++++ database/seeders/UserSeeder.php | 2 +- resources/views/dashboard.blade.php | 38 +++++ resources/views/users/index.blade.php | 4 +- resources/views/users/show.blade.php | 99 +++++++++++++ routes/web.php | 1 + tests/Feature/AdvanceTest.php | 130 ++++++++++++++++++ 12 files changed, 388 insertions(+), 7 deletions(-) create mode 100644 app/Models/UserAdvanceBalance.php create mode 100644 database/migrations/2026_08_29_000001_create_user_advance_balances_table.php create mode 100644 resources/views/users/show.blade.php 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 +

+ +
+ + + + + + + + + + @forelse($lowestAdvanceBalances ?? [] as $index => $user) + + + + + + @empty + + + + @endforelse + +
#PracownikBilans
{{ $index + 1 }} + + {{ $user->name }} {{ $user->surname }} + + + {{ number_format((float) ($user->advanceBalance?->balance ?? 0), 2, ',', ' ') }} zł +
Brak danych o bilansach zaliczek.
+
+
+ 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 @@ + + +
+

+ Profil pracownika +

+ + + Powrót do listy + +
+
+ +
+
+
+
+
+
+

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 +
+ + + + + + + + + + @foreach($user->advances->sortByDesc('date') as $advance) + + + + + + @endforeach + +
DataTypKwota
{{ $advance->date->format('d.m.Y') }}{{ $advance->type?->name ?? '—' }} + {{ number_format((float) $advance->amount, 2, ',', ' ') }} zł +
+
+ @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'); +});