Dodanie floty do profila pracownika

This commit is contained in:
Tomasz Boruc 2026-08-30 21:51:28 +02:00
parent 4c9142d6ad
commit cd1234a118
8 changed files with 253 additions and 1 deletions

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\User; use App\Models\User;
use App\Models\Vehicle;
use Illuminate\View\View; use Illuminate\View\View;
use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Role;
@ -38,13 +39,23 @@ class DashboardController extends Controller
->take(10) ->take(10)
->values(); ->values();
$vehiclesDueSoon = Vehicle::with('user')
->where(function ($query) {
$query->whereBetween('oc_expiry_date', [now()->startOfDay(), now()->addDays(14)->endOfDay()])
->orWhereBetween('technical_inspection_date', [now()->startOfDay(), now()->addDays(14)->endOfDay()]);
})
->orderBy('oc_expiry_date')
->orderBy('technical_inspection_date')
->get();
return view('dashboard', compact( return view('dashboard', compact(
'totalUsers', 'totalUsers',
'adminCount', 'adminCount',
'managerCount', 'managerCount',
'workerCount', 'workerCount',
'recentUsers', 'recentUsers',
'lowestAdvanceBalances' 'lowestAdvanceBalances',
'vehiclesDueSoon'
)); ));
} }
} }

View File

@ -61,6 +61,11 @@ class User extends Authenticatable
return $this->hasMany(Advance::class); return $this->hasMany(Advance::class);
} }
public function vehicles(): HasMany
{
return $this->hasMany(Vehicle::class);
}
public function advanceBalance(): HasOne public function advanceBalance(): HasOne
{ {
return $this->hasOne(UserAdvanceBalance::class); return $this->hasOne(UserAdvanceBalance::class);

View File

@ -0,0 +1,60 @@
<?php
namespace Database\Seeders;
use App\Models\Advance;
use App\Models\AdvanceType;
use App\Models\User;
use Illuminate\Database\Seeder;
class AdvanceSeeder extends Seeder
{
public function run(): void
{
$users = User::query()->orderBy('id')->pluck('id')->all();
$types = AdvanceType::query()->pluck('id', 'name')->all();
if (empty($users) || empty($types)) {
return;
}
$entries = [
['user_index' => 0, 'type' => 'Paliwo', 'amount' => 180.50, 'days_ago' => 1],
['user_index' => 1, 'type' => 'Hotel', 'amount' => 240.00, 'days_ago' => 2],
['user_index' => 2, 'type' => 'Wpłata', 'amount' => 500.00, 'days_ago' => 3],
['user_index' => 0, 'type' => 'Paliwo', 'amount' => 95.20, 'days_ago' => 6],
['user_index' => 1, 'type' => 'Hotel', 'amount' => 310.40, 'days_ago' => 8],
['user_index' => 3, 'type' => 'Paliwo', 'amount' => 150.10, 'days_ago' => 9],
['user_index' => 4, 'type' => 'Wpłata', 'amount' => 750.00, 'days_ago' => 10],
['user_index' => 2, 'type' => 'Hotel', 'amount' => 420.00, 'days_ago' => 12],
['user_index' => 5, 'type' => 'Paliwo', 'amount' => 205.30, 'days_ago' => 14],
['user_index' => 6, 'type' => 'Wpłata', 'amount' => 600.00, 'days_ago' => 15],
['user_index' => 0, 'type' => 'Hotel', 'amount' => 280.00, 'days_ago' => 17],
['user_index' => 1, 'type' => 'Paliwo', 'amount' => 120.00, 'days_ago' => 18],
['user_index' => 3, 'type' => 'Wpłata', 'amount' => 900.00, 'days_ago' => 20],
['user_index' => 4, 'type' => 'Hotel', 'amount' => 360.75, 'days_ago' => 22],
['user_index' => 5, 'type' => 'Paliwo', 'amount' => 175.25, 'days_ago' => 24],
['user_index' => 6, 'type' => 'Wpłata', 'amount' => 850.00, 'days_ago' => 26],
['user_index' => 2, 'type' => 'Paliwo', 'amount' => 165.15, 'days_ago' => 28],
['user_index' => 0, 'type' => 'Wpłata', 'amount' => 1200.00, 'days_ago' => 29],
['user_index' => 1, 'type' => 'Hotel', 'amount' => 540.20, 'days_ago' => 30],
['user_index' => 3, 'type' => 'Paliwo', 'amount' => 210.45, 'days_ago' => 33],
];
foreach ($entries as $index => $entry) {
$userId = $users[$entry['user_index']] ?? $users[0];
$typeId = $types[$entry['type']] ?? null;
if (! $typeId) {
continue;
}
Advance::create([
'date' => now()->subDays($entry['days_ago'])->toDateString(),
'user_id' => $userId,
'amount' => $entry['amount'],
'advance_type_id' => $typeId,
]);
}
}
}

View File

@ -53,6 +53,7 @@ class DatabaseSeeder extends Seeder
$this->call([ $this->call([
AdvanceTypeSeeder::class, AdvanceTypeSeeder::class,
UserSeeder::class, UserSeeder::class,
AdvanceSeeder::class,
VehicleSeeder::class, VehicleSeeder::class,
]); ]);
} }

View File

@ -133,6 +133,58 @@
</div> </div>
</div> </div>
{{-- Samochody do uwagi --}}
<div class="bg-amber-50 dark:bg-amber-900/20 p-6 rounded-lg shadow-sm border border-amber-200 dark:border-amber-800/60 lg:col-span-3">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-amber-900 dark:text-amber-200">
Samochody do uwagi
</h3>
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-amber-100 dark:bg-amber-800/40 text-amber-800 dark:text-amber-200">
do 14 dni
</span>
</div>
@if(($vehiclesDueSoon ?? collect())->isNotEmpty())
<div class="space-y-3">
@foreach($vehiclesDueSoon as $vehicle)
@php
$ocDue = $vehicle->oc_expiry_date && $vehicle->oc_expiry_date->between(now()->startOfDay(), now()->addDays(14)->endOfDay());
$inspectionDue = $vehicle->technical_inspection_date && $vehicle->technical_inspection_date->between(now()->startOfDay(), now()->addDays(14)->endOfDay());
@endphp
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 rounded-lg border border-amber-200 dark:border-amber-700/60 bg-white/70 dark:bg-gray-900/30 p-4">
<div>
<p class="font-semibold text-gray-900 dark:text-white">
{{ $vehicle->brand }} {{ $vehicle->model ?? '' }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-300">
{{ $vehicle->registration_number }} · {{ $vehicle->user?->name }} {{ $vehicle->user?->surname }}
</p>
</div>
<div class="flex flex-wrap gap-2 text-xs font-medium">
@if($ocDue)
<span class="inline-flex items-center px-2.5 py-1 rounded-full bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-200">
OC: {{ $vehicle->oc_expiry_date->format('d.m.Y') }}
</span>
@endif
@if($inspectionDue)
<span class="inline-flex items-center px-2.5 py-1 rounded-full bg-orange-100 dark:bg-orange-900/40 text-orange-700 dark:text-orange-200">
Przegląd: {{ $vehicle->technical_inspection_date->format('d.m.Y') }}
</span>
@endif
</div>
</div>
@endforeach
</div>
@else
<p class="text-sm text-amber-800 dark:text-amber-200">
Brak pojazdów z terminem OC lub przeglądu w najbliższych 14 dniach.
</p>
@endif
</div>
{{-- Najniższy bilans zaliczek --}} {{-- Najniższy bilans zaliczek --}}
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700/50 lg:col-span-3"> <div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700/50 lg:col-span-3">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4"> <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">

View File

@ -85,6 +85,43 @@
</dl> </dl>
</div> </div>
<div class="employee-profile-info bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg border border-gray-200 dark:border-gray-700/50 p-6">
<h4 class="text-lg font-semibold mb-4">Przypisany pojazd</h4>
@php($assignedVehicle = $user->vehicles()->first())
@if($assignedVehicle)
<dl class="space-y-3 text-sm">
<div>
<dt class="text-gray-500 dark:text-gray-400">Samochód</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ $assignedVehicle->brand }} {{ $assignedVehicle->model }}</dd>
</div>
<div>
<dt class="text-gray-500 dark:text-gray-400">Nr rejestracyjny</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ $assignedVehicle->registration_number }}</dd>
</div>
<div>
<dt class="text-gray-500 dark:text-gray-400">Status</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ $assignedVehicle->status }}</dd>
</div>
<div>
<dt class="text-gray-500 dark:text-gray-400">OC</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ $assignedVehicle->oc_expiry_date?->format('d.m.Y') ?? '—' }}</dd>
</div>
<div>
<dt class="text-gray-500 dark:text-gray-400">Przegląd</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ $assignedVehicle->technical_inspection_date?->format('d.m.Y') ?? '—' }}</dd>
</div>
<div>
<dt class="text-gray-500 dark:text-gray-400">Ubezpieczenie</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ $assignedVehicle->insurance_provider ?? '—' }}</dd>
</div>
</dl>
@else
<p class="text-sm text-gray-500 dark:text-gray-400">Brak przypisanego pojazdu.</p>
@endif
</div>
<div class="employee-profile-finance lg:col-span-2 bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg border border-gray-200 dark:border-gray-700/50 p-6"> <div class="employee-profile-finance lg:col-span-2 bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg border border-gray-200 dark:border-gray-700/50 p-6">
<h4 class="text-lg font-semibold mb-4">Podsumowanie finansowe</h4> <h4 class="text-lg font-semibold mb-4">Podsumowanie finansowe</h4>

View File

@ -20,6 +20,14 @@ function userWithAdvanceAccess(string $roleName = 'pracownik'): User
return $user; return $user;
} }
test('database seeder creates 20 sample advances', function () {
$this->seed();
$seededAdvances = Advance::count();
expect($seededAdvances)->toBe(20);
});
test('employee sees only own advances', function () { test('employee sees only own advances', function () {
$employee = userWithAdvanceAccess(); $employee = userWithAdvanceAccess();
$otherUser = User::factory()->create(); $otherUser = User::factory()->create();

View File

@ -129,3 +129,81 @@ test('manager can delete a vehicle', function () {
$response->assertRedirect(route('flota')); $response->assertRedirect(route('flota'));
$this->assertDatabaseMissing('vehicles', ['id' => $vehicle->id]); $this->assertDatabaseMissing('vehicles', ['id' => $vehicle->id]);
}); });
test('dashboard shows vehicles with upcoming technical inspection or oc expiry within 14 days', function () {
$manager = createFleetUser('kierownik');
$user = User::factory()->create();
$urgentVehicle = Vehicle::create([
'brand' => 'Toyota',
'model' => 'Corolla',
'registration_number' => 'DW1234A',
'user_id' => $user->id,
'year' => 2021,
'purchase_date' => '2021-05-12',
'oc_expiry_date' => now()->addDays(10)->toDateString(),
'oil_change_km' => 12000,
'insurance_provider' => 'PZU',
'technical_inspection_date' => now()->addDays(20)->toDateString(),
'mileage_km' => 95000,
'status' => 'używany',
]);
$safeVehicle = Vehicle::create([
'brand' => 'Ford',
'model' => 'Transit',
'registration_number' => 'WA9876Z',
'user_id' => $user->id,
'year' => 2019,
'purchase_date' => '2019-07-20',
'oc_expiry_date' => now()->addDays(30)->toDateString(),
'oil_change_km' => 8000,
'insurance_provider' => 'Warta',
'technical_inspection_date' => now()->addDays(40)->toDateString(),
'mileage_km' => 175000,
'status' => 'aktywny',
]);
$response = $this->actingAs($manager)->get(route('dashboard'));
$response->assertOk();
$response->assertSee('Samochody do uwagi');
$response->assertSee($urgentVehicle->brand);
$response->assertSee('OC');
$response->assertDontSee($safeVehicle->brand);
});
test('employee profile shows assigned vehicle with expire dates and insurance details', function () {
Permission::firstOrCreate(['name' => 'users_access']);
$manager = User::factory()->create();
$managerRole = Role::firstOrCreate(['name' => 'kierownik']);
$managerRole->givePermissionTo(['users_access', 'flota_access']);
$manager->assignRole($managerRole);
$user = User::factory()->create();
$vehicle = Vehicle::create([
'brand' => 'BMW',
'model' => 'X5',
'registration_number' => 'PO1111A',
'user_id' => $user->id,
'year' => 2022,
'purchase_date' => '2022-02-10',
'oc_expiry_date' => now()->addDays(9)->toDateString(),
'oil_change_km' => 12000,
'insurance_provider' => 'PZU',
'technical_inspection_date' => now()->addDays(12)->toDateString(),
'mileage_km' => 55000,
'status' => 'używany',
]);
$response = $this->actingAs($manager)->get(route('users.show', $user));
$response->assertOk();
$response->assertSee('Przypisany pojazd');
$response->assertSee($vehicle->brand);
$response->assertSee('PZU');
$response->assertSee('OC');
$response->assertSee('Przegląd');
});