@fleetbase/fleetops-engine 0.6.16 → 0.6.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/addon/components/layout/fleet-ops-sidebar.hbs +25 -0
  2. package/addon/templates/virtual.hbs +3 -3
  3. package/composer.json +3 -2
  4. package/extension.json +1 -1
  5. package/package.json +1 -1
  6. package/server/migrations/2025_08_11_170800_add_company_index_to_routes_table.php +21 -0
  7. package/server/migrations/2025_08_28_054920_create_warranties_table.php +56 -0
  8. package/server/migrations/2025_08_28_054921_create_telematics_table.php +60 -0
  9. package/server/migrations/2025_08_28_054922_create_assets_table.php +94 -0
  10. package/server/migrations/2025_08_28_054925_create_devices_table.php +190 -0
  11. package/server/migrations/2025_08_28_054926_create_device_events_table.php +99 -0
  12. package/server/migrations/2025_08_28_054926_create_sensors_table.php +62 -0
  13. package/server/migrations/2025_08_28_054927_create_parts_table.php +73 -0
  14. package/server/migrations/2025_08_28_054929_create_equipments_table.php +58 -0
  15. package/server/migrations/2025_08_28_054930_create_work_orders_table.php +85 -0
  16. package/server/migrations/2025_08_28_054931_create_maintenances_table.php +79 -0
  17. package/server/migrations/2025_08_28_082002_update_vehicles_table_telematics.php +60 -0
  18. package/server/src/Http/Controllers/Api/v1/OrderController.php +19 -1
  19. package/server/src/Http/Controllers/Internal/v1/OrderController.php +31 -8
  20. package/server/src/Http/Resources/v1/Order.php +111 -60
  21. package/server/src/Models/Asset.php +548 -0
  22. package/server/src/Models/Contact.php +2 -0
  23. package/server/src/Models/Device.php +435 -0
  24. package/server/src/Models/DeviceEvent.php +501 -0
  25. package/server/src/Models/Driver.php +2 -0
  26. package/server/src/Models/Entity.php +27 -50
  27. package/server/src/Models/Equipment.php +483 -0
  28. package/server/src/Models/Fleet.php +2 -0
  29. package/server/src/Models/FuelReport.php +2 -0
  30. package/server/src/Models/Issue.php +2 -0
  31. package/server/src/Models/Maintenance.php +549 -0
  32. package/server/src/Models/Order.php +32 -112
  33. package/server/src/Models/OrderConfig.php +8 -0
  34. package/server/src/Models/Part.php +502 -0
  35. package/server/src/Models/Payload.php +101 -20
  36. package/server/src/Models/Place.php +10 -4
  37. package/server/src/Models/Sensor.php +510 -0
  38. package/server/src/Models/ServiceArea.php +1 -1
  39. package/server/src/Models/Telematic.php +336 -0
  40. package/server/src/Models/Vehicle.php +45 -1
  41. package/server/src/Models/VehicleDevice.php +1 -1
  42. package/server/src/Models/Vendor.php +2 -0
  43. package/server/src/Models/Warranty.php +413 -0
  44. package/server/src/Models/Waypoint.php +2 -0
  45. package/server/src/Models/WorkOrder.php +532 -0
  46. package/server/src/Support/Utils.php +5 -0
  47. package/server/src/Traits/HasTrackingNumber.php +64 -10
  48. package/server/src/Traits/Maintainable.php +307 -0
  49. package/server/src/Traits/PayloadAccessors.php +126 -0
@@ -0,0 +1,307 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Traits;
4
+
5
+ use Fleetbase\FleetOps\Models\Maintenance;
6
+ use Illuminate\Database\Eloquent\Relations\MorphMany;
7
+
8
+ /**
9
+ * Trait Maintainable.
10
+ *
11
+ * Provides maintenance-related functionality for models that can be maintained.
12
+ * This trait can be used by Asset, Equipment, and other maintainable entities.
13
+ */
14
+ trait Maintainable
15
+ {
16
+ /**
17
+ * Get all maintenances for this maintainable entity.
18
+ */
19
+ public function maintenances(): MorphMany
20
+ {
21
+ return $this->morphMany(Maintenance::class, 'maintainable');
22
+ }
23
+
24
+ /**
25
+ * Get scheduled maintenances.
26
+ */
27
+ public function scheduledMaintenances(): MorphMany
28
+ {
29
+ return $this->maintenances()->where('status', 'scheduled');
30
+ }
31
+
32
+ /**
33
+ * Get completed maintenances.
34
+ */
35
+ public function completedMaintenances(): MorphMany
36
+ {
37
+ return $this->maintenances()->where('status', 'completed');
38
+ }
39
+
40
+ /**
41
+ * Get overdue maintenances.
42
+ */
43
+ public function overdueMaintenances(): MorphMany
44
+ {
45
+ return $this->maintenances()
46
+ ->where('status', 'scheduled')
47
+ ->where('scheduled_at', '<', now());
48
+ }
49
+
50
+ /**
51
+ * Get the last completed maintenance.
52
+ */
53
+ public function getLastMaintenanceAttribute(): ?Maintenance
54
+ {
55
+ return $this->completedMaintenances()
56
+ ->orderBy('completed_at', 'desc')
57
+ ->first();
58
+ }
59
+
60
+ /**
61
+ * Get the next scheduled maintenance.
62
+ */
63
+ public function getNextMaintenanceAttribute(): ?Maintenance
64
+ {
65
+ return $this->scheduledMaintenances()
66
+ ->orderBy('scheduled_at', 'asc')
67
+ ->first();
68
+ }
69
+
70
+ /**
71
+ * Check if the entity needs maintenance.
72
+ */
73
+ public function needsMaintenance(): bool
74
+ {
75
+ // Check if there's overdue maintenance
76
+ if ($this->overdueMaintenances()->exists()) {
77
+ return true;
78
+ }
79
+
80
+ // Check maintenance intervals based on usage metrics
81
+ return $this->checkMaintenanceIntervals();
82
+ }
83
+
84
+ /**
85
+ * Check maintenance intervals based on usage metrics.
86
+ */
87
+ protected function checkMaintenanceIntervals(): bool
88
+ {
89
+ $lastMaintenance = $this->last_maintenance;
90
+
91
+ if (!$lastMaintenance) {
92
+ // If no maintenance history, check against purchase/creation date
93
+ $baseDate = $this->purchased_at ?? $this->created_at;
94
+
95
+ return $this->checkIntervalsSinceDate($baseDate);
96
+ }
97
+
98
+ // Check intervals since last maintenance
99
+ return $this->checkIntervalsSinceDate($lastMaintenance->completed_at);
100
+ }
101
+
102
+ /**
103
+ * Check maintenance intervals since a specific date.
104
+ *
105
+ * @param \Carbon\Carbon $date
106
+ */
107
+ protected function checkIntervalsSinceDate($date): bool
108
+ {
109
+ $specs = $this->specs ?? [];
110
+ $meta = $this->meta ?? [];
111
+
112
+ // Check time-based intervals
113
+ $maintenanceIntervalDays = $specs['maintenance_interval_days'] ?? $meta['maintenance_interval_days'] ?? null;
114
+ if ($maintenanceIntervalDays && $date->diffInDays(now()) >= $maintenanceIntervalDays) {
115
+ return true;
116
+ }
117
+
118
+ // Check odometer-based intervals (for assets with odometer)
119
+ if (isset($this->odometer) && isset($specs['maintenance_interval_miles'])) {
120
+ $lastOdometer = $this->last_maintenance?->odometer ?? 0;
121
+ $milesSinceLastMaintenance = $this->odometer - $lastOdometer;
122
+ if ($milesSinceLastMaintenance >= $specs['maintenance_interval_miles']) {
123
+ return true;
124
+ }
125
+ }
126
+
127
+ // Check engine hours-based intervals (for assets with engine hours)
128
+ if (isset($this->engine_hours) && isset($specs['maintenance_interval_hours'])) {
129
+ $lastEngineHours = $this->last_maintenance?->engine_hours ?? 0;
130
+ $hoursSinceLastMaintenance = $this->engine_hours - $lastEngineHours;
131
+ if ($hoursSinceLastMaintenance >= $specs['maintenance_interval_hours']) {
132
+ return true;
133
+ }
134
+ }
135
+
136
+ return false;
137
+ }
138
+
139
+ /**
140
+ * Schedule maintenance for the entity.
141
+ */
142
+ public function scheduleMaintenance(string $type, \DateTime $scheduledAt, array $details = []): Maintenance
143
+ {
144
+ return Maintenance::create([
145
+ 'company_uuid' => $this->company_uuid,
146
+ 'maintainable_type' => static::class,
147
+ 'maintainable_uuid' => $this->uuid,
148
+ 'type' => $type,
149
+ 'status' => 'scheduled',
150
+ 'scheduled_at' => $scheduledAt,
151
+ 'odometer' => $this->odometer ?? null,
152
+ 'engine_hours' => $this->engine_hours ?? null,
153
+ 'summary' => $details['summary'] ?? null,
154
+ 'notes' => $details['notes'] ?? null,
155
+ 'priority' => $details['priority'] ?? 'medium',
156
+ 'created_by_uuid' => auth()->id(),
157
+ ]);
158
+ }
159
+
160
+ /**
161
+ * Get maintenance cost for a specific period.
162
+ */
163
+ public function getMaintenanceCost(int $days = 365): float
164
+ {
165
+ $startDate = now()->subDays($days);
166
+
167
+ return $this->completedMaintenances()
168
+ ->where('completed_at', '>=', $startDate)
169
+ ->sum('total_cost') ?? 0;
170
+ }
171
+
172
+ /**
173
+ * Get maintenance frequency (maintenances per year).
174
+ */
175
+ public function getMaintenanceFrequency(int $days = 365): float
176
+ {
177
+ $startDate = now()->subDays($days);
178
+ $maintenanceCount = $this->completedMaintenances()
179
+ ->where('completed_at', '>=', $startDate)
180
+ ->count();
181
+
182
+ return ($maintenanceCount / $days) * 365;
183
+ }
184
+
185
+ /**
186
+ * Get average maintenance duration in hours.
187
+ */
188
+ public function getAverageMaintenanceDuration(int $days = 365): ?float
189
+ {
190
+ $startDate = now()->subDays($days);
191
+ $maintenances = $this->completedMaintenances()
192
+ ->where('completed_at', '>=', $startDate)
193
+ ->whereNotNull('started_at')
194
+ ->whereNotNull('completed_at')
195
+ ->get();
196
+
197
+ if ($maintenances->isEmpty()) {
198
+ return null;
199
+ }
200
+
201
+ $totalHours = $maintenances->sum(function ($maintenance) {
202
+ return $maintenance->started_at->diffInHours($maintenance->completed_at);
203
+ });
204
+
205
+ return $totalHours / $maintenances->count();
206
+ }
207
+
208
+ /**
209
+ * Get maintenance efficiency rating.
210
+ */
211
+ public function getMaintenanceEfficiency(int $days = 365): ?float
212
+ {
213
+ $startDate = now()->subDays($days);
214
+ $maintenances = $this->completedMaintenances()
215
+ ->where('completed_at', '>=', $startDate)
216
+ ->get();
217
+
218
+ if ($maintenances->isEmpty()) {
219
+ return null;
220
+ }
221
+
222
+ $onTimeCount = $maintenances->filter(function ($maintenance) {
223
+ return $maintenance->wasCompletedOnTime();
224
+ })->count();
225
+
226
+ return ($onTimeCount / $maintenances->count()) * 100;
227
+ }
228
+
229
+ /**
230
+ * Get upcoming maintenance due dates.
231
+ *
232
+ * @return \Illuminate\Database\Eloquent\Collection
233
+ */
234
+ public function getUpcomingMaintenance(int $days = 30)
235
+ {
236
+ $endDate = now()->addDays($days);
237
+
238
+ return $this->scheduledMaintenances()
239
+ ->where('scheduled_at', '<=', $endDate)
240
+ ->orderBy('scheduled_at', 'asc')
241
+ ->get();
242
+ }
243
+
244
+ /**
245
+ * Create preventive maintenance schedule.
246
+ */
247
+ public function createPreventiveMaintenanceSchedule(array $intervals = []): array
248
+ {
249
+ $schedule = [];
250
+ $specs = $this->specs ?? [];
251
+ $defaultIntervals = $specs['maintenance_intervals'] ?? [];
252
+
253
+ $intervals = array_merge($defaultIntervals, $intervals);
254
+
255
+ foreach ($intervals as $type => $interval) {
256
+ $lastMaintenance = $this->completedMaintenances()
257
+ ->where('type', $type)
258
+ ->orderBy('completed_at', 'desc')
259
+ ->first();
260
+
261
+ $baseDate = $lastMaintenance?->completed_at ?? $this->created_at;
262
+ $nextDue = $baseDate->copy()->addDays($interval['days'] ?? 365);
263
+
264
+ if ($nextDue->isFuture()) {
265
+ $schedule[] = [
266
+ 'type' => $type,
267
+ 'due_date' => $nextDue,
268
+ 'interval_days' => $interval['days'] ?? 365,
269
+ 'priority' => $interval['priority'] ?? 'medium',
270
+ 'description' => $interval['description'] ?? "Scheduled {$type} maintenance",
271
+ ];
272
+ }
273
+ }
274
+
275
+ return $schedule;
276
+ }
277
+
278
+ /**
279
+ * Get maintenance history summary.
280
+ */
281
+ public function getMaintenanceHistorySummary(int $days = 365): array
282
+ {
283
+ $startDate = now()->subDays($days);
284
+ $maintenances = $this->maintenances()
285
+ ->where('created_at', '>=', $startDate)
286
+ ->get();
287
+
288
+ $completed = $maintenances->where('status', 'completed');
289
+ $scheduled = $maintenances->where('status', 'scheduled');
290
+ $overdue = $scheduled->filter(function ($maintenance) {
291
+ return $maintenance->scheduled_at->isPast();
292
+ });
293
+
294
+ return [
295
+ 'total_maintenances' => $maintenances->count(),
296
+ 'completed_count' => $completed->count(),
297
+ 'scheduled_count' => $scheduled->count(),
298
+ 'overdue_count' => $overdue->count(),
299
+ 'total_cost' => $completed->sum('total_cost'),
300
+ 'average_cost' => $completed->avg('total_cost'),
301
+ 'total_downtime_hours' => $completed->sum('duration_hours'),
302
+ 'average_duration_hours' => $completed->avg('duration_hours'),
303
+ 'on_time_percentage' => $this->getMaintenanceEfficiency($days),
304
+ 'most_common_type' => $completed->groupBy('type')->sortByDesc->count()->keys()->first(),
305
+ ];
306
+ }
307
+ }
@@ -0,0 +1,126 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Traits;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Fleetbase\FleetOps\Models\Payload;
7
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
8
+ use Illuminate\Support\Str;
9
+
10
+ /**
11
+ * PayloadAccessors.
12
+ *
13
+ * Drop this trait into the model that has the `payload()` relationship and a `payload_uuid` FK.
14
+ * It provides:
15
+ * - getPayload(): returns the related Payload if present (with global scopes applied)
16
+ * - getTrashedPayload(): returns the related Payload without global scopes (e.g., soft-deleted)
17
+ * - getOrder(): returns the Order associated through Payload (or null)
18
+ *
19
+ * Design notes / best practices:
20
+ * - Avoids unconditional eager loading; checks relation cache first via relationLoaded().
21
+ * - Uses the relationship query as the primary source of truth, falling back to a direct lookup by UUID.
22
+ * - When resolving via direct lookup, caches the relation with setRelation() to prevent repeat queries.
23
+ * - Keeps the “ignore global scopes” behavior in a single private helper.
24
+ *
25
+ * Assumptions:
26
+ * - The host model defines `public function payload(): BelongsTo`.
27
+ * - `payload_uuid` stores a UUID, and `Payload`’s primary key is configured for UUIDs (so `find()` works).
28
+ */
29
+ trait PayloadAccessors
30
+ {
31
+ /**
32
+ * Get the associated Payload with global scopes applied.
33
+ *
34
+ * @return \App\Models\Payload|null
35
+ */
36
+ public function getPayload(): ?Payload
37
+ {
38
+ return $this->resolvePayload(ignoreGlobalScopes: false);
39
+ }
40
+
41
+ /**
42
+ * Get the associated Payload without global scopes (e.g., include soft-deleted).
43
+ *
44
+ * @return \App\Models\Payload|null
45
+ */
46
+ public function getTrashedPayload(): ?Payload
47
+ {
48
+ return $this->resolvePayload(ignoreGlobalScopes: true);
49
+ }
50
+
51
+ /**
52
+ * Convenience accessor: get the Order via the associated Payload.
53
+ *
54
+ * @return \App\Models\Order|null
55
+ */
56
+ public function getOrder(): ?Order
57
+ {
58
+ // Leverage PHP nullsafe operator; no extra branching needed.
59
+ return $this->getPayload()?->order;
60
+ }
61
+
62
+ /**
63
+ * Core resolver for the Payload relation.
64
+ *
65
+ * Strategy:
66
+ * 1) If the relation is already loaded and we are NOT ignoring global scopes, return it.
67
+ * 2) Otherwise, query via the relationship, optionally disabling global scopes.
68
+ * 3) If still not found and payload_uuid looks like a UUID, do a direct lookup on Payload,
69
+ * optionally disabling global scopes. If found, cache back into the relation.
70
+ *
71
+ * @param bool $ignoreGlobalScopes when true, fetch via `withoutGlobalScopes()`
72
+ *
73
+ * @return \App\Models\Payload|null
74
+ */
75
+ protected function resolvePayload(bool $ignoreGlobalScopes = false): ?Payload
76
+ {
77
+ // (1) If relation is already loaded and we accept scoped data, return it.
78
+ if (!$ignoreGlobalScopes && $this->relationLoaded('payload')) {
79
+ $related = $this->getRelation('payload');
80
+ if ($related instanceof Payload) {
81
+ return $related;
82
+ }
83
+ }
84
+
85
+ // (2) Query through the relationship (preferred, keeps FK semantics consistent).
86
+ $relation = $this->payload();
87
+ $query = $ignoreGlobalScopes ? $relation->withoutGlobalScopes() : $relation;
88
+
89
+ /** @var \App\Models\Payload|null $payload */
90
+ $payload = $query->first();
91
+
92
+ if ($payload instanceof Payload) {
93
+ // Cache the resolved relation for subsequent access on this model instance.
94
+ $this->setRelation('payload', $payload);
95
+
96
+ return $payload;
97
+ }
98
+
99
+ // (3) Fallback: direct lookup by UUID (useful if relation is not properly hydrated).
100
+ $uuid = $this->payload_uuid ?? null;
101
+ if ($uuid && Str::isUuid($uuid)) {
102
+ $payloadQuery = Payload::query();
103
+ if ($ignoreGlobalScopes) {
104
+ $payloadQuery->withoutGlobalScopes();
105
+ }
106
+
107
+ $payload = $payloadQuery->find($uuid); // relies on Payload PK being 'uuid'
108
+
109
+ if ($payload instanceof Payload) {
110
+ // Keep the relation cache consistent for this instance.
111
+ $this->setRelation('payload', $payload);
112
+
113
+ return $payload;
114
+ }
115
+ }
116
+
117
+ return null;
118
+ }
119
+
120
+ /**
121
+ * Relationship stub (documentational).
122
+ * Ensure your model actually defines this in the host model—not strictly required here,
123
+ * but included as a guide for expected signature.
124
+ */
125
+ abstract public function payload(): BelongsTo;
126
+ }