Skip to content

Commit bcca3d0

Browse files
committed
Phases 191-195: Fleet Management Module — 16 tests passing
4 migrations (fleet_vehicles, fleet_fuel_logs, fleet_maintenances, fleet_vehicle_assignments — prefixed to avoid Inventory vehicles collision), 4 models (Vehicle/FuelLog/VehicleMaintenance/VehicleAssignment with expiry detection and cost aggregation), FleetPolicy, 4 controllers (Dashboard/ Vehicle/FuelLog/Maintenance), 7 React pages, Sidebar Fleet section. 16/16 feature tests passing. https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 9cb73e1 commit bcca3d0

25 files changed

Lines changed: 2446 additions & 0 deletions

erp/app/Modules/Core/Providers/CoreServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use App\Modules\POS\Providers\POSServiceProvider;
1616
use App\Modules\Helpdesk\Providers\HelpdeskServiceProvider;
1717
use App\Modules\Accounting\Providers\AccountingServiceProvider;
18+
use App\Modules\Fleet\Providers\FleetServiceProvider;
1819
use Illuminate\Support\Facades\Gate;
1920
use Illuminate\Support\ServiceProvider;
2021

@@ -31,6 +32,7 @@ public function register(): void
3132
$this->app->register(POSServiceProvider::class);
3233
$this->app->register(HelpdeskServiceProvider::class);
3334
$this->app->register(AccountingServiceProvider::class);
35+
$this->app->register(FleetServiceProvider::class);
3436
}
3537

3638
public function boot(): void
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace App\Modules\Fleet\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Fleet\Models\FuelLog;
7+
use App\Modules\Fleet\Models\Vehicle;
8+
use App\Modules\Fleet\Models\VehicleMaintenance;
9+
use Inertia\Inertia;
10+
use Inertia\Response;
11+
12+
class FleetDashboardController extends Controller
13+
{
14+
public function index(): Response
15+
{
16+
$totalVehicles = Vehicle::count();
17+
$activeVehicles = Vehicle::where('status', 'active')->count();
18+
$inService = Vehicle::where('status', 'in_service')->count();
19+
20+
$monthlyFuelCost = FuelLog::whereYear('log_date', now()->year)
21+
->whereMonth('log_date', now()->month)
22+
->sum('total_cost');
23+
24+
$expiringInsurance = Vehicle::whereNotNull('insurance_expiry')
25+
->whereDate('insurance_expiry', '>', now())
26+
->whereDate('insurance_expiry', '<=', now()->addDays(30))
27+
->count();
28+
29+
$upcomingMaintenances = VehicleMaintenance::where('status', 'scheduled')
30+
->whereDate('due_date', '>=', now())
31+
->whereDate('due_date', '<=', now()->addDays(30))
32+
->count();
33+
34+
$recentFuelLogs = FuelLog::with(['vehicle', 'driver'])
35+
->orderByDesc('log_date')
36+
->limit(5)
37+
->get();
38+
39+
return Inertia::render('Fleet/Dashboard', [
40+
'stats' => [
41+
'totalVehicles' => $totalVehicles,
42+
'activeVehicles' => $activeVehicles,
43+
'inService' => $inService,
44+
'monthlyFuelCost' => (float) $monthlyFuelCost,
45+
'expiringInsurance' => $expiringInsurance,
46+
'upcomingMaintenances' => $upcomingMaintenances,
47+
],
48+
'recentFuelLogs' => $recentFuelLogs,
49+
]);
50+
}
51+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace App\Modules\Fleet\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Fleet\Models\FuelLog;
7+
use App\Modules\Fleet\Models\Vehicle;
8+
use Illuminate\Http\RedirectResponse;
9+
use Illuminate\Http\Request;
10+
use Inertia\Inertia;
11+
use Inertia\Response;
12+
13+
class FuelLogController extends Controller
14+
{
15+
public function index(Request $request): Response
16+
{
17+
$fuelLogs = FuelLog::with(['vehicle', 'driver'])
18+
->when($request->vehicle_id, fn ($q) => $q->where('vehicle_id', $request->vehicle_id))
19+
->orderByDesc('log_date')
20+
->paginate(25)
21+
->withQueryString();
22+
23+
return Inertia::render('Fleet/FuelLogs/Index', [
24+
'fuelLogs' => $fuelLogs,
25+
'vehicles' => Vehicle::orderBy('name')->get(['id', 'name']),
26+
'filters' => $request->only(['vehicle_id']),
27+
]);
28+
}
29+
30+
public function store(Request $request): RedirectResponse
31+
{
32+
$validated = $request->validate([
33+
'vehicle_id' => 'required|exists:fleet_vehicles,id',
34+
'log_date' => 'required|date',
35+
'odometer_km' => 'required|numeric|min:0',
36+
'liters' => 'required|numeric|min:0',
37+
'cost_per_liter' => 'required|numeric|min:0',
38+
'total_cost' => 'nullable|numeric|min:0',
39+
'fuel_type' => 'nullable|string|max:50',
40+
'station' => 'nullable|string|max:255',
41+
'driver_id' => 'nullable|exists:users,id',
42+
'notes' => 'nullable|string',
43+
]);
44+
45+
if (empty($validated['total_cost'])) {
46+
$validated['total_cost'] = round($validated['liters'] * $validated['cost_per_liter'], 2);
47+
}
48+
49+
FuelLog::create([
50+
...$validated,
51+
'tenant_id' => auth()->user()->tenant_id,
52+
]);
53+
54+
return redirect()->back()->with('success', 'Fuel log added.');
55+
}
56+
57+
public function destroy(FuelLog $fuelLog): RedirectResponse
58+
{
59+
$fuelLog->delete();
60+
61+
return redirect()->back()->with('success', 'Fuel log deleted.');
62+
}
63+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
namespace App\Modules\Fleet\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Models\User;
7+
use App\Modules\Fleet\Models\Vehicle;
8+
use Illuminate\Http\RedirectResponse;
9+
use Illuminate\Http\Request;
10+
use Inertia\Inertia;
11+
use Inertia\Response;
12+
13+
class VehicleController extends Controller
14+
{
15+
public function index(): Response
16+
{
17+
$vehicles = Vehicle::with('assignedDriver')
18+
->withCount([
19+
'maintenances as due_soon_count' => function ($q) {
20+
$q->where('status', 'scheduled')
21+
->whereDate('due_date', '>=', now())
22+
->whereDate('due_date', '<=', now()->addDays(30));
23+
},
24+
])
25+
->orderBy('name')
26+
->get();
27+
28+
return Inertia::render('Fleet/Vehicles/Index', [
29+
'vehicles' => $vehicles,
30+
]);
31+
}
32+
33+
public function create(): Response
34+
{
35+
return Inertia::render('Fleet/Vehicles/Create', [
36+
'users' => User::orderBy('name')->get(['id', 'name']),
37+
]);
38+
}
39+
40+
public function store(Request $request): RedirectResponse
41+
{
42+
$validated = $request->validate([
43+
'name' => 'required|string|max:255',
44+
'plate_number' => 'nullable|string|max:50',
45+
'make' => 'nullable|string|max:100',
46+
'model' => 'nullable|string|max:100',
47+
'year' => 'nullable|integer|min:1900|max:2100',
48+
'color' => 'nullable|string|max:50',
49+
'vin' => 'nullable|string|max:50',
50+
'type' => 'required|in:car,truck,van,motorcycle,other',
51+
'status' => 'required|in:active,in_service,out_of_service,sold',
52+
'odometer_km' => 'nullable|numeric|min:0',
53+
'fuel_type' => 'required|in:petrol,diesel,electric,hybrid',
54+
'assigned_to' => 'nullable|exists:users,id',
55+
'insurance_expiry' => 'nullable|date',
56+
'registration_expiry' => 'nullable|date',
57+
'notes' => 'nullable|string',
58+
]);
59+
60+
$vehicle = Vehicle::create([
61+
...$validated,
62+
'tenant_id' => auth()->user()->tenant_id,
63+
]);
64+
65+
return redirect()->route('fleet.vehicles.show', $vehicle)->with('success', 'Vehicle created.');
66+
}
67+
68+
public function show(Vehicle $vehicle): Response
69+
{
70+
$vehicle->load(['assignedDriver', 'fuelLogs.driver', 'maintenances', 'assignments.driver']);
71+
72+
$recentFuelLogs = $vehicle->fuelLogs()
73+
->with('driver')
74+
->orderByDesc('log_date')
75+
->limit(5)
76+
->get();
77+
78+
$upcomingMaintenances = $vehicle->maintenances()
79+
->whereIn('status', ['scheduled', 'in_progress'])
80+
->orderBy('due_date')
81+
->get();
82+
83+
return Inertia::render('Fleet/Vehicles/Show', [
84+
'vehicle' => $vehicle,
85+
'recentFuelLogs' => $recentFuelLogs,
86+
'upcomingMaintenances' => $upcomingMaintenances,
87+
'users' => User::orderBy('name')->get(['id', 'name']),
88+
]);
89+
}
90+
91+
public function edit(Vehicle $vehicle): Response
92+
{
93+
return Inertia::render('Fleet/Vehicles/Edit', [
94+
'vehicle' => $vehicle,
95+
'users' => User::orderBy('name')->get(['id', 'name']),
96+
]);
97+
}
98+
99+
public function update(Request $request, Vehicle $vehicle): RedirectResponse
100+
{
101+
$validated = $request->validate([
102+
'name' => 'required|string|max:255',
103+
'plate_number' => 'nullable|string|max:50',
104+
'make' => 'nullable|string|max:100',
105+
'model' => 'nullable|string|max:100',
106+
'year' => 'nullable|integer|min:1900|max:2100',
107+
'color' => 'nullable|string|max:50',
108+
'vin' => 'nullable|string|max:50',
109+
'type' => 'required|in:car,truck,van,motorcycle,other',
110+
'status' => 'required|in:active,in_service,out_of_service,sold',
111+
'odometer_km' => 'nullable|numeric|min:0',
112+
'fuel_type' => 'required|in:petrol,diesel,electric,hybrid',
113+
'assigned_to' => 'nullable|exists:users,id',
114+
'insurance_expiry' => 'nullable|date',
115+
'registration_expiry' => 'nullable|date',
116+
'notes' => 'nullable|string',
117+
]);
118+
119+
$vehicle->update($validated);
120+
121+
return redirect()->route('fleet.vehicles.show', $vehicle)->with('success', 'Vehicle updated.');
122+
}
123+
124+
public function destroy(Vehicle $vehicle): RedirectResponse
125+
{
126+
$vehicle->delete();
127+
128+
return redirect()->route('fleet.vehicles.index')->with('success', 'Vehicle deleted.');
129+
}
130+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace App\Modules\Fleet\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Fleet\Models\Vehicle;
7+
use App\Modules\Fleet\Models\VehicleMaintenance;
8+
use Illuminate\Http\RedirectResponse;
9+
use Illuminate\Http\Request;
10+
use Inertia\Inertia;
11+
use Inertia\Response;
12+
13+
class VehicleMaintenanceController extends Controller
14+
{
15+
public function index(Request $request): Response
16+
{
17+
$maintenances = VehicleMaintenance::with('vehicle')
18+
->when($request->status, fn ($q) => $q->where('status', $request->status))
19+
->when($request->vehicle_id, fn ($q) => $q->where('vehicle_id', $request->vehicle_id))
20+
->orderByDesc('service_date')
21+
->paginate(25)
22+
->withQueryString();
23+
24+
return Inertia::render('Fleet/Maintenances/Index', [
25+
'maintenances' => $maintenances,
26+
'vehicles' => Vehicle::orderBy('name')->get(['id', 'name']),
27+
'filters' => $request->only(['status', 'vehicle_id']),
28+
]);
29+
}
30+
31+
public function store(Request $request): RedirectResponse
32+
{
33+
$validated = $request->validate([
34+
'vehicle_id' => 'required|exists:fleet_vehicles,id',
35+
'type' => 'required|in:scheduled,repair,inspection,other',
36+
'description' => 'nullable|string',
37+
'vendor' => 'nullable|string|max:255',
38+
'service_date' => 'required|date',
39+
'due_date' => 'nullable|date',
40+
'odometer_km' => 'nullable|numeric|min:0',
41+
'cost' => 'nullable|numeric|min:0',
42+
'status' => 'required|in:scheduled,in_progress,completed,cancelled',
43+
'notes' => 'nullable|string',
44+
]);
45+
46+
VehicleMaintenance::create([
47+
...$validated,
48+
'tenant_id' => auth()->user()->tenant_id,
49+
'created_by' => auth()->id(),
50+
]);
51+
52+
return redirect()->back()->with('success', 'Maintenance record added.');
53+
}
54+
55+
public function complete(VehicleMaintenance $maintenance): RedirectResponse
56+
{
57+
$maintenance->complete();
58+
59+
return redirect()->back()->with('success', 'Maintenance marked as completed.');
60+
}
61+
62+
public function destroy(VehicleMaintenance $maintenance): RedirectResponse
63+
{
64+
$maintenance->delete();
65+
66+
return redirect()->back()->with('success', 'Maintenance record deleted.');
67+
}
68+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace App\Modules\Fleet\Models;
4+
5+
use App\Models\User;
6+
use App\Modules\Core\Traits\BelongsToTenant;
7+
use Illuminate\Database\Eloquent\Model;
8+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
9+
10+
class FuelLog extends Model
11+
{
12+
use BelongsToTenant;
13+
14+
protected $table = 'fleet_fuel_logs';
15+
16+
protected $fillable = [
17+
'tenant_id',
18+
'vehicle_id',
19+
'log_date',
20+
'odometer_km',
21+
'liters',
22+
'cost_per_liter',
23+
'total_cost',
24+
'fuel_type',
25+
'station',
26+
'driver_id',
27+
'notes',
28+
];
29+
30+
protected $casts = [
31+
'log_date' => 'date',
32+
'liters' => 'float',
33+
'cost_per_liter' => 'float',
34+
'total_cost' => 'float',
35+
'odometer_km' => 'float',
36+
];
37+
38+
public function vehicle(): BelongsTo
39+
{
40+
return $this->belongsTo(Vehicle::class, 'vehicle_id');
41+
}
42+
43+
public function driver(): BelongsTo
44+
{
45+
return $this->belongsTo(User::class, 'driver_id');
46+
}
47+
}

0 commit comments

Comments
 (0)