Dodanei mudułu flota

This commit is contained in:
Tomasz Boruc 2026-08-30 21:26:00 +02:00
parent d550bab596
commit 6079a491f7
10 changed files with 765 additions and 4 deletions

View File

@ -0,0 +1,131 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Vehicle;
use Illuminate\Http\Request;
use Illuminate\View\View;
class VehicleController extends Controller
{
public function index(Request $request): View
{
$query = Vehicle::with('user');
if ($request->user()->hasRole('pracownik')) {
$query->where('user_id', $request->user()->id);
}
$search = trim((string) $request->query('search', ''));
if ($search !== '') {
$query->where(function ($query) use ($search) {
$query->where('brand', 'like', "%{$search}%")
->orWhere('model', 'like', "%{$search}%")
->orWhere('registration_number', 'like', "%{$search}%")
->orWhere('insurance_provider', 'like', "%{$search}%")
->orWhere('status', 'like', "%{$search}%")
->orWhereHas('user', function ($userQuery) use ($search) {
$userQuery->where('name', 'like', "%{$search}%")
->orWhere('surname', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
});
}
$vehicles = $query->orderBy('brand')->orderBy('registration_number')->paginate(15)->withQueryString();
$users = User::orderBy('name')->orderBy('surname')->get();
return view('flota', compact('vehicles', 'users'));
}
public function create(Request $request): View
{
$this->authorizeVehicleManagement($request);
$users = User::orderBy('name')->orderBy('surname')->get();
return view('flota.create', compact('users'));
}
public function store(Request $request)
{
$this->authorizeVehicleManagement($request);
$validated = $request->validate([
'brand' => ['required', 'string', 'max:255'],
'model' => ['nullable', 'string', 'max:255'],
'registration_number' => ['required', 'string', 'max:50', 'unique:vehicles,registration_number'],
'user_id' => ['required', 'integer', 'exists:users,id'],
'year' => ['required', 'integer', 'min:1900', 'max:2100'],
'purchase_date' => ['required', 'date'],
'oc_expiry_date' => ['required', 'date'],
'oil_change_km' => ['required', 'integer', 'min:0'],
'insurance_provider' => ['required', 'string', 'max:255'],
'technical_inspection_date' => ['required', 'date'],
'mileage_km' => ['required', 'integer', 'min:0'],
'status' => ['required', 'string', 'in:aktywny,serwis,używany,sprzedany'],
]);
Vehicle::create($validated);
return redirect()->route('flota')->with('status', 'Pojazd został dodany.');
}
public function edit(Request $request, Vehicle $vehicle): View
{
$this->authorizeVehicleManagement($request);
$this->authorizeVehicleAccess($request, $vehicle);
$users = User::orderBy('name')->orderBy('surname')->get();
return view('flota.edit', compact('vehicle', 'users'));
}
public function update(Request $request, Vehicle $vehicle)
{
$this->authorizeVehicleManagement($request);
$this->authorizeVehicleAccess($request, $vehicle);
$validated = $request->validate([
'brand' => ['required', 'string', 'max:255'],
'model' => ['nullable', 'string', 'max:255'],
'registration_number' => ['required', 'string', 'max:50', 'unique:vehicles,registration_number,' . $vehicle->id],
'user_id' => ['required', 'integer', 'exists:users,id'],
'year' => ['required', 'integer', 'min:1900', 'max:2100'],
'purchase_date' => ['required', 'date'],
'oc_expiry_date' => ['required', 'date'],
'oil_change_km' => ['required', 'integer', 'min:0'],
'insurance_provider' => ['required', 'string', 'max:255'],
'technical_inspection_date' => ['required', 'date'],
'mileage_km' => ['required', 'integer', 'min:0'],
'status' => ['required', 'string', 'in:aktywny,serwis,używany,sprzedany'],
]);
$vehicle->update($validated);
return redirect()->route('flota')->with('status', 'Pojazd został zaktualizowany.');
}
public function destroy(Request $request, Vehicle $vehicle)
{
$this->authorizeVehicleManagement($request);
$this->authorizeVehicleAccess($request, $vehicle);
$vehicle->delete();
return redirect()->route('flota')->with('status', 'Pojazd został usunięty.');
}
private function authorizeVehicleManagement(Request $request): void
{
abort_unless($request->user()->can('flota_access'), 403);
}
private function authorizeVehicleAccess(Request $request, Vehicle $vehicle): void
{
if ($request->user()->hasRole('pracownik') && $vehicle->user_id !== $request->user()->id) {
abort(403);
}
}
}

44
app/Models/Vehicle.php Normal file
View File

@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Vehicle extends Model
{
use HasFactory;
protected $fillable = [
'brand',
'model',
'registration_number',
'user_id',
'year',
'purchase_date',
'oc_expiry_date',
'oil_change_km',
'insurance_provider',
'technical_inspection_date',
'mileage_km',
'status',
];
protected function casts(): array
{
return [
'purchase_date' => 'date',
'oc_expiry_date' => 'date',
'technical_inspection_date' => 'date',
'year' => 'integer',
'oil_change_km' => 'integer',
'mileage_km' => 'integer',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('vehicles', function (Blueprint $table) {
$table->id();
$table->string('brand');
$table->string('model')->nullable();
$table->string('registration_number')->unique();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->unsignedSmallInteger('year');
$table->date('purchase_date');
$table->date('oc_expiry_date');
$table->unsignedInteger('oil_change_km');
$table->string('insurance_provider');
$table->date('technical_inspection_date');
$table->unsignedInteger('mileage_km');
$table->string('status');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('vehicles');
}
};

View File

@ -31,6 +31,7 @@ class DatabaseSeeder extends Seeder
$this->call([ $this->call([
AdvanceTypeSeeder::class, AdvanceTypeSeeder::class,
VehicleSeeder::class,
]); ]);
// 2. Tworzenie ról (Roles) // 2. Tworzenie ról (Roles)

View File

@ -0,0 +1,126 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use App\Models\Vehicle;
use Illuminate\Database\Seeder;
class VehicleSeeder extends Seeder
{
public function run(): void
{
$users = User::query()->orderBy('name')->orderBy('surname')->get();
if ($users->isEmpty()) {
return;
}
$sampleVehicles = [
[
'brand' => 'Toyota',
'model' => 'Corolla',
'registration_number' => 'DW1234A',
'user_id' => $users->first()->id,
'year' => 2021,
'purchase_date' => '2021-05-12',
'oc_expiry_date' => '2027-05-12',
'oil_change_km' => 12000,
'insurance_provider' => 'PZU',
'technical_inspection_date' => '2026-12-01',
'mileage_km' => 95000,
'status' => 'używany',
],
[
'brand' => 'Ford',
'model' => 'Transit',
'registration_number' => 'WA9876Z',
'user_id' => $users->get(1)->id ?? $users->first()->id,
'year' => 2019,
'purchase_date' => '2019-07-20',
'oc_expiry_date' => '2027-07-20',
'oil_change_km' => 8000,
'insurance_provider' => 'Warta',
'technical_inspection_date' => '2026-09-15',
'mileage_km' => 175000,
'status' => 'aktywny',
],
[
'brand' => 'BMW',
'model' => 'X5',
'registration_number' => 'PO1111A',
'user_id' => $users->last()->id,
'year' => 2022,
'purchase_date' => '2022-02-18',
'oc_expiry_date' => '2028-02-18',
'oil_change_km' => 15000,
'insurance_provider' => 'PZU',
'technical_inspection_date' => '2027-02-11',
'mileage_km' => 64000,
'status' => 'serwis',
],
[
'brand' => 'Mercedes',
'model' => 'Vito',
'registration_number' => 'GD2044K',
'user_id' => $users->first()->id,
'year' => 2020,
'purchase_date' => '2020-11-04',
'oc_expiry_date' => '2027-11-04',
'oil_change_km' => 10000,
'insurance_provider' => 'PZU',
'technical_inspection_date' => '2026-10-20',
'mileage_km' => 118500,
'status' => 'aktywny',
],
[
'brand' => 'Volkswagen',
'model' => 'Crafter',
'registration_number' => 'LU5567N',
'user_id' => $users->get(1)->id ?? $users->first()->id,
'year' => 2018,
'purchase_date' => '2018-06-15',
'oc_expiry_date' => '2026-12-15',
'oil_change_km' => 9000,
'insurance_provider' => 'Warta',
'technical_inspection_date' => '2026-08-30',
'mileage_km' => 214000,
'status' => 'serwis',
],
[
'brand' => 'Skoda',
'model' => 'Superb',
'registration_number' => 'TR9912R',
'user_id' => $users->last()->id,
'year' => 2023,
'purchase_date' => '2023-03-22',
'oc_expiry_date' => '2028-03-22',
'oil_change_km' => 18000,
'insurance_provider' => 'PZU',
'technical_inspection_date' => '2027-04-12',
'mileage_km' => 42000,
'status' => 'używany',
],
[
'brand' => 'Renault',
'model' => 'Traffic',
'registration_number' => 'WK3345P',
'user_id' => $users->first()->id,
'year' => 2017,
'purchase_date' => '2017-09-01',
'oc_expiry_date' => '2026-09-01',
'oil_change_km' => 11000,
'insurance_provider' => 'Hestia',
'technical_inspection_date' => '2026-11-05',
'mileage_km' => 188400,
'status' => 'sprzedany',
],
];
foreach ($sampleVehicles as $vehicle) {
Vehicle::firstOrCreate([
'registration_number' => $vehicle['registration_number'],
], $vehicle);
}
}
}

View File

@ -5,11 +5,113 @@
</h2> </h2>
</x-slot> </x-slot>
@php
$showActions = !auth()->user()->hasRole('pracownik');
@endphp
<div class="py-12"> <div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-[1800px] mx-auto sm:px-6 lg:px-8 space-y-6">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg p-6 text-gray-900 dark:text-gray-100"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg p-6 text-gray-900 dark:text-gray-100">
Tu będzie Flota <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<h3 class="text-lg font-semibold">Lista pojazdów</h3>
@if(!auth()->user()->hasRole('pracownik'))
<a href="{{ route('flota.create') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md">
+ Dodaj pojazd
</a>
@endif
</div> </div>
<div class="mt-5 mb-6 flex items-center justify-between gap-4">
<form method="GET" action="{{ route('flota') }}" class="flex items-center gap-2 w-full max-w-xl">
<div class="w-full">
<input type="text" name="search" value="{{ request('search') }}" placeholder="Szukaj po marce, modelu, numerze rejestracyjnym..." class="block w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-700 rounded-md shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
</div>
<button type="submit" 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 duration-150">
Szukaj
</button>
@if(request('search'))
<a href="{{ route('flota') }}" 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>
@if(session('status'))
<div class="rounded-md bg-green-100 border border-green-300 text-green-800 px-4 py-3">
{{ session('status') }}
</div>
@endif
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg text-gray-900 dark:text-gray-100">
<div class="overflow-x-auto">
<table class="min-w-[1400px] w-full table-fixed divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-900">
<tr>
<th class="px-4 py-3 text-left text-xs uppercase">Marka</th>
<th class="px-4 py-3 text-left text-xs uppercase">Model</th>
<th class="px-4 py-3 text-left text-xs uppercase">Nr rejestracyjny</th>
<th class="px-4 py-3 text-left text-xs uppercase">Właściciel</th>
<th class="px-4 py-3 text-left text-xs uppercase">Rok</th>
<th class="px-4 py-3 text-left text-xs uppercase">Zakup</th>
<th class="px-4 py-3 text-left text-xs uppercase">OC do</th>
<th class="px-4 py-3 text-left text-xs uppercase">Ubezpieczyciel</th>
<th class="px-4 py-3 text-left text-xs uppercase">Przegląd</th>
<th class="px-4 py-3 text-left text-xs uppercase">Olej</th>
<th class="px-4 py-3 text-left text-xs uppercase">Przebieg</th>
<th class="px-4 py-3 text-left text-xs uppercase">Status</th>
@if($showActions)
<th class="px-4 py-3 text-left text-xs uppercase">Akcje</th>
@endif
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($vehicles as $vehicle)
<tr>
<td class="px-4 py-3">{{ $vehicle->brand }}</td>
<td class="px-4 py-3">{{ $vehicle->model ?? '-' }}</td>
<td class="px-4 py-3">{{ $vehicle->registration_number }}</td>
<td class="px-4 py-3">{{ $vehicle->user ? $vehicle->user->name.' '.$vehicle->user->surname : '-' }}</td>
<td class="px-4 py-3">{{ $vehicle->year }}</td>
<td class="px-4 py-3">{{ $vehicle->purchase_date?->format('d.m.Y') ?? '-' }}</td>
<td class="px-4 py-3">{{ $vehicle->oc_expiry_date?->format('d.m.Y') ?? '-' }}</td>
<td class="px-4 py-3">{{ $vehicle->insurance_provider ?? '-' }}</td>
<td class="px-4 py-3">{{ $vehicle->technical_inspection_date?->format('d.m.Y') ?? '-' }}</td>
<td class="px-4 py-3">{{ number_format($vehicle->oil_change_km, 0, ',', ' ') }} km</td>
<td class="px-4 py-3">{{ number_format($vehicle->mileage_km, 0, ',', ' ') }} km</td>
<td class="px-4 py-3">
<span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold bg-gray-200 text-gray-800">
{{ $vehicle->status }}
</span>
</td>
@if($showActions)
<td class="px-4 py-3 space-x-2">
<a href="{{ route('flota.edit', $vehicle) }}" class="text-blue-600 hover:underline">Edytuj</a>
<form method="POST" action="{{ route('flota.destroy', $vehicle) }}" class="inline-block" onsubmit="return confirm('Usunąć pojazd?');">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:underline">Usuń</button>
</form>
</td>
@endif
</tr>
@empty
<tr>
<td colspan="{{ $showActions ? 13 : 12 }}" class="px-4 py-6 text-center text-gray-500">Brak pojazdów do wyświetlenia.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="p-4">
{{ $vehicles->links() }}
</div>
</div>
</div> </div>
</div> </div>
</x-app-layout> </x-app-layout>

View File

@ -0,0 +1,93 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Dodaj pojazd') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-4xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg p-6 text-gray-900 dark:text-gray-100">
<form method="POST" action="{{ route('flota.store') }}" class="grid grid-cols-1 md:grid-cols-2 gap-4">
@csrf
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Marka</label>
<input type="text" name="brand" value="{{ old('brand') }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Model</label>
<input type="text" name="model" value="{{ old('model') }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400">
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Nr rejestracyjny</label>
<input type="text" name="registration_number" value="{{ old('registration_number') }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Właściciel</label>
<select name="user_id" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
<option value="" class="dark:bg-gray-900">Wybierz użytkownika</option>
@foreach($users as $user)
<option value="{{ $user->id }}" {{ old('user_id') == $user->id ? 'selected' : '' }} class="dark:bg-gray-900">
{{ $user->name }} {{ $user->surname }}
</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Rok</label>
<input type="number" name="year" value="{{ old('year') }}" min="1900" max="2100" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Data zakupu</label>
<input type="date" name="purchase_date" value="{{ old('purchase_date') }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Data wygaśnięcia OC</label>
<input type="date" name="oc_expiry_date" value="{{ old('oc_expiry_date') }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Przebieg ostatniej wymiany oleju</label>
<input type="number" name="oil_change_km" value="{{ old('oil_change_km') }}" min="0" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Ubezpieczyciel</label>
<input type="text" name="insurance_provider" value="{{ old('insurance_provider') }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Data przeglądu technicznego</label>
<input type="date" name="technical_inspection_date" value="{{ old('technical_inspection_date') }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Przebieg</label>
<input type="number" name="mileage_km" value="{{ old('mileage_km') }}" min="0" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Status</label>
<select name="status" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
@foreach(['aktywny', 'serwis', 'używany', 'sprzedany'] as $status)
<option value="{{ $status }}" {{ old('status') == $status ? 'selected' : '' }} class="dark:bg-gray-900">{{ $status }}</option>
@endforeach
</select>
</div>
<div class="md:col-span-2 flex justify-end gap-3">
<a href="{{ route('flota') }}" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700">Anuluj</a>
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">Dodaj pojazd</button>
</div>
</form>
</div>
</div>
</div>
</x-app-layout>

View File

@ -0,0 +1,94 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Edytuj pojazd') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-4xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg p-6 text-gray-900 dark:text-gray-100">
<form method="POST" action="{{ route('flota.update', $vehicle) }}" class="grid grid-cols-1 md:grid-cols-2 gap-4">
@csrf
@method('PUT')
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Marka</label>
<input type="text" name="brand" value="{{ old('brand', $vehicle->brand) }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Model</label>
<input type="text" name="model" value="{{ old('model', $vehicle->model) }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400">
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Nr rejestracyjny</label>
<input type="text" name="registration_number" value="{{ old('registration_number', $vehicle->registration_number) }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Właściciel</label>
<select name="user_id" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
<option value="" class="dark:bg-gray-900">Wybierz użytkownika</option>
@foreach($users as $user)
<option value="{{ $user->id }}" {{ old('user_id', $vehicle->user_id) == $user->id ? 'selected' : '' }} class="dark:bg-gray-900">
{{ $user->name }} {{ $user->surname }}
</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Rok</label>
<input type="number" name="year" value="{{ old('year', $vehicle->year) }}" min="1900" max="2100" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Data zakupu</label>
<input type="date" name="purchase_date" value="{{ old('purchase_date', $vehicle->purchase_date->format('Y-m-d')) }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Data wygaśnięcia OC</label>
<input type="date" name="oc_expiry_date" value="{{ old('oc_expiry_date', $vehicle->oc_expiry_date->format('Y-m-d')) }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Przebieg ostatniej wymiany oleju</label>
<input type="number" name="oil_change_km" value="{{ old('oil_change_km', $vehicle->oil_change_km) }}" min="0" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Ubezpieczyciel</label>
<input type="text" name="insurance_provider" value="{{ old('insurance_provider', $vehicle->insurance_provider) }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Data przeglądu technicznego</label>
<input type="date" name="technical_inspection_date" value="{{ old('technical_inspection_date', $vehicle->technical_inspection_date->format('Y-m-d')) }}" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Przebieg</label>
<input type="number" name="mileage_km" value="{{ old('mileage_km', $vehicle->mileage_km) }}" min="0" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
</div>
<div>
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-200">Status</label>
<select name="status" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400" required>
@foreach(['aktywny', 'serwis', 'używany', 'sprzedany'] as $status)
<option value="{{ $status }}" {{ old('status', $vehicle->status) == $status ? 'selected' : '' }} class="dark:bg-gray-900">{{ $status }}</option>
@endforeach
</select>
</div>
<div class="md:col-span-2 flex justify-end gap-3">
<a href="{{ route('flota') }}" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700">Anuluj</a>
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">Zapisz zmiany</button>
</div>
</form>
</div>
</div>
</div>
</x-app-layout>

View File

@ -5,6 +5,7 @@ use App\Http\Controllers\AdvanceController;
use App\Http\Controllers\ProfileController; use App\Http\Controllers\ProfileController;
use App\Http\Controllers\SettingsController; use App\Http\Controllers\SettingsController;
use App\Http\Controllers\UserController; use App\Http\Controllers\UserController;
use App\Http\Controllers\VehicleController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/', function () { Route::get('/', function () {
@ -50,7 +51,12 @@ Route::middleware(['auth', 'verified'])->group(function () {
// Moduł Flota // Moduł Flota
Route::middleware(['can:flota_access'])->group(function () { Route::middleware(['can:flota_access'])->group(function () {
Route::view('/flota', 'flota')->name('flota'); Route::get('/flota', [VehicleController::class, 'index'])->name('flota');
Route::get('/flota/create', [VehicleController::class, 'create'])->name('flota.create');
Route::post('/flota', [VehicleController::class, 'store'])->name('flota.store');
Route::get('/flota/{vehicle}/edit', [VehicleController::class, 'edit'])->name('flota.edit');
Route::put('/flota/{vehicle}', [VehicleController::class, 'update'])->name('flota.update');
Route::delete('/flota/{vehicle}', [VehicleController::class, 'destroy'])->name('flota.destroy');
}); });
// Moduł Zaliczki // Moduł Zaliczki

View File

@ -0,0 +1,131 @@
<?php
use App\Models\User;
use App\Models\Vehicle;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
beforeEach(function () {
Permission::firstOrCreate(['name' => 'flota_access']);
});
function createFleetUser(string $roleName = 'pracownik'): User
{
$user = User::factory()->create();
$role = Role::firstOrCreate(['name' => $roleName]);
$role->givePermissionTo('flota_access');
$user->assignRole($role);
return $user;
}
test('manager sees all vehicles and employee sees only assigned vehicles', function () {
$manager = createFleetUser('kierownik');
$employee = createFleetUser('pracownik');
$assignedVehicle = Vehicle::create([
'brand' => 'Toyota',
'registration_number' => 'DW1234A',
'user_id' => $employee->id,
'year' => 2020,
'purchase_date' => '2020-06-01',
'oc_expiry_date' => '2027-06-01',
'oil_change_km' => 10000,
'insurance_provider' => 'Warta',
'technical_inspection_date' => '2026-09-15',
'mileage_km' => 120000,
'status' => 'używany',
]);
$otherVehicle = Vehicle::create([
'brand' => 'Ford',
'registration_number' => 'WA9876Z',
'user_id' => $manager->id,
'year' => 2018,
'purchase_date' => '2018-03-01',
'oc_expiry_date' => '2027-03-01',
'oil_change_km' => 8000,
'insurance_provider' => 'PZU',
'technical_inspection_date' => '2026-10-01',
'mileage_km' => 180000,
'status' => 'używany',
]);
$managerResponse = $this->actingAs($manager)->get(route('flota'));
$managerResponse->assertOk();
$managerResponse->assertSee('Toyota');
$managerResponse->assertSee('Ford');
$employeeResponse = $this->actingAs($employee)->get(route('flota'));
$employeeResponse->assertOk();
$employeeResponse->assertSee('Toyota');
$employeeResponse->assertDontSee('Ford');
$this->assertDatabaseHas('vehicles', ['id' => $assignedVehicle->id, 'brand' => 'Toyota']);
$this->assertDatabaseHas('vehicles', ['id' => $otherVehicle->id, 'brand' => 'Ford']);
});
test('manager can create and edit a vehicle', function () {
$manager = createFleetUser('kierownik');
$user = User::factory()->create();
$createResponse = $this->actingAs($manager)->post(route('flota.store'), [
'brand' => 'BMW',
'registration_number' => 'PO1111A',
'user_id' => $user->id,
'year' => 2022,
'purchase_date' => '2022-02-10',
'oc_expiry_date' => '2027-02-10',
'oil_change_km' => 15000,
'insurance_provider' => 'PZU',
'technical_inspection_date' => '2026-11-20',
'mileage_km' => 50000,
'status' => 'używany',
]);
$createResponse->assertRedirect(route('flota'));
$this->assertDatabaseHas('vehicles', ['registration_number' => 'PO1111A', 'brand' => 'BMW']);
$vehicle = Vehicle::where('registration_number', 'PO1111A')->first();
$editResponse = $this->actingAs($manager)->put(route('flota.update', $vehicle), [
'brand' => 'BMW',
'registration_number' => 'PO1111A',
'user_id' => $user->id,
'year' => 2023,
'purchase_date' => '2022-02-10',
'oc_expiry_date' => '2028-02-10',
'oil_change_km' => 15000,
'insurance_provider' => 'Warta',
'technical_inspection_date' => '2027-11-20',
'mileage_km' => 60000,
'status' => 'sprzedany',
]);
$editResponse->assertRedirect(route('flota'));
$this->assertDatabaseHas('vehicles', ['id' => $vehicle->id, 'year' => 2023, 'status' => 'sprzedany']);
});
test('manager can delete a vehicle', function () {
$manager = createFleetUser('kierownik');
$user = User::factory()->create();
$vehicle = Vehicle::create([
'brand' => 'Audi',
'registration_number' => 'KR9999X',
'user_id' => $user->id,
'year' => 2019,
'purchase_date' => '2019-01-01',
'oc_expiry_date' => '2027-01-01',
'oil_change_km' => 12000,
'insurance_provider' => 'Warta',
'technical_inspection_date' => '2026-12-01',
'mileage_km' => 99000,
'status' => 'używany',
]);
$response = $this->actingAs($manager)->delete(route('flota.destroy', $vehicle));
$response->assertRedirect(route('flota'));
$this->assertDatabaseMissing('vehicles', ['id' => $vehicle->id]);
});