@fleetbase/fleetops-engine 0.6.17 → 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 (44) 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_28_054920_create_warranties_table.php +56 -0
  7. package/server/migrations/2025_08_28_054921_create_telematics_table.php +60 -0
  8. package/server/migrations/2025_08_28_054922_create_assets_table.php +94 -0
  9. package/server/migrations/2025_08_28_054925_create_devices_table.php +190 -0
  10. package/server/migrations/2025_08_28_054926_create_device_events_table.php +99 -0
  11. package/server/migrations/2025_08_28_054926_create_sensors_table.php +62 -0
  12. package/server/migrations/2025_08_28_054927_create_parts_table.php +73 -0
  13. package/server/migrations/2025_08_28_054929_create_equipments_table.php +58 -0
  14. package/server/migrations/2025_08_28_054930_create_work_orders_table.php +85 -0
  15. package/server/migrations/2025_08_28_054931_create_maintenances_table.php +79 -0
  16. package/server/migrations/2025_08_28_082002_update_vehicles_table_telematics.php +60 -0
  17. package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -1
  18. package/server/src/Http/Resources/v1/Order.php +111 -60
  19. package/server/src/Models/Asset.php +548 -0
  20. package/server/src/Models/Contact.php +2 -0
  21. package/server/src/Models/Device.php +435 -0
  22. package/server/src/Models/DeviceEvent.php +501 -0
  23. package/server/src/Models/Driver.php +2 -0
  24. package/server/src/Models/Entity.php +2 -0
  25. package/server/src/Models/Equipment.php +483 -0
  26. package/server/src/Models/Fleet.php +2 -0
  27. package/server/src/Models/FuelReport.php +2 -0
  28. package/server/src/Models/Issue.php +2 -0
  29. package/server/src/Models/Maintenance.php +549 -0
  30. package/server/src/Models/Order.php +32 -112
  31. package/server/src/Models/OrderConfig.php +8 -0
  32. package/server/src/Models/Part.php +502 -0
  33. package/server/src/Models/Payload.php +101 -20
  34. package/server/src/Models/Place.php +10 -4
  35. package/server/src/Models/Sensor.php +510 -0
  36. package/server/src/Models/ServiceArea.php +1 -1
  37. package/server/src/Models/Telematic.php +336 -0
  38. package/server/src/Models/Vehicle.php +45 -1
  39. package/server/src/Models/VehicleDevice.php +1 -1
  40. package/server/src/Models/Vendor.php +2 -0
  41. package/server/src/Models/Warranty.php +413 -0
  42. package/server/src/Models/WorkOrder.php +532 -0
  43. package/server/src/Support/Utils.php +5 -0
  44. package/server/src/Traits/Maintainable.php +307 -0
@@ -0,0 +1,549 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Models;
4
+
5
+ use Fleetbase\Casts\Json;
6
+ use Fleetbase\Models\Model;
7
+ use Fleetbase\Models\User;
8
+ use Fleetbase\Traits\HasApiModelBehavior;
9
+ use Fleetbase\Traits\HasCustomFields;
10
+ use Fleetbase\Traits\HasMetaAttributes;
11
+ use Fleetbase\Traits\HasPublicId;
12
+ use Fleetbase\Traits\HasUuid;
13
+ use Fleetbase\Traits\Searchable;
14
+ use Fleetbase\Traits\TracksApiCredential;
15
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
16
+ use Illuminate\Database\Eloquent\Relations\MorphTo;
17
+ use Spatie\Activitylog\LogOptions;
18
+ use Spatie\Activitylog\Traits\LogsActivity;
19
+
20
+ /**
21
+ * Class Maintenance.
22
+ *
23
+ * Tracks service actions performed on maintainable entities (Assets, Equipment).
24
+ * Includes scheduling, execution, costs, and documentation of maintenance activities.
25
+ */
26
+ class Maintenance extends Model
27
+ {
28
+ use HasUuid;
29
+ use HasPublicId;
30
+ use TracksApiCredential;
31
+ use HasApiModelBehavior;
32
+ use LogsActivity;
33
+ use HasMetaAttributes;
34
+ use Searchable;
35
+ use HasCustomFields;
36
+
37
+ /**
38
+ * The database table used by the model.
39
+ *
40
+ * @var string
41
+ */
42
+ protected $table = 'maintenances';
43
+
44
+ /**
45
+ * The type of public Id to generate.
46
+ *
47
+ * @var string
48
+ */
49
+ protected $publicIdType = 'maintenance';
50
+
51
+ /**
52
+ * The attributes that can be queried.
53
+ *
54
+ * @var array
55
+ */
56
+ protected $searchableColumns = ['summary', 'notes', 'type', 'public_id'];
57
+
58
+ /**
59
+ * The attributes that can be used for filtering.
60
+ *
61
+ * @var array
62
+ */
63
+ protected $filterParams = ['type', 'status', 'priority', 'maintainable_type', 'work_order_uuid', 'performed_by_type'];
64
+
65
+ /**
66
+ * The attributes that are mass assignable.
67
+ *
68
+ * @var array
69
+ */
70
+ protected $fillable = [
71
+ 'company_uuid',
72
+ 'work_order_uuid',
73
+ 'maintainable_type',
74
+ 'maintainable_uuid',
75
+ 'type',
76
+ 'status',
77
+ 'priority',
78
+ 'scheduled_at',
79
+ 'started_at',
80
+ 'completed_at',
81
+ 'odometer',
82
+ 'engine_hours',
83
+ 'performed_by_type',
84
+ 'performed_by_uuid',
85
+ 'summary',
86
+ 'notes',
87
+ 'line_items',
88
+ 'labor_cost',
89
+ 'parts_cost',
90
+ 'tax',
91
+ 'total_cost',
92
+ 'attachments',
93
+ 'meta',
94
+ 'slug',
95
+ ];
96
+
97
+ /**
98
+ * Dynamic attributes that are appended to object.
99
+ *
100
+ * @var array
101
+ */
102
+ protected $appends = [
103
+ 'maintainable_name',
104
+ 'work_order_subject',
105
+ 'performed_by_name',
106
+ 'duration_hours',
107
+ 'is_overdue',
108
+ 'days_until_due',
109
+ 'cost_breakdown',
110
+ ];
111
+
112
+ /**
113
+ * The attributes excluded from the model's JSON form.
114
+ *
115
+ * @var array
116
+ */
117
+ protected $hidden = ['maintainable', 'workOrder', 'performedBy'];
118
+
119
+ /**
120
+ * The attributes that should be cast to native types.
121
+ *
122
+ * @var array
123
+ */
124
+ protected $casts = [
125
+ 'scheduled_at' => 'datetime',
126
+ 'started_at' => 'datetime',
127
+ 'completed_at' => 'datetime',
128
+ 'odometer' => 'integer',
129
+ 'engine_hours' => 'integer',
130
+ 'labor_cost' => 'decimal:2',
131
+ 'parts_cost' => 'decimal:2',
132
+ 'tax' => 'decimal:2',
133
+ 'total_cost' => 'decimal:2',
134
+ 'line_items' => Json::class,
135
+ 'attachments' => Json::class,
136
+ 'meta' => Json::class,
137
+ ];
138
+
139
+ /**
140
+ * Properties which activity needs to be logged.
141
+ *
142
+ * @var array
143
+ */
144
+ protected static $logAttributes = '*';
145
+
146
+ /**
147
+ * Do not log empty changed.
148
+ *
149
+ * @var bool
150
+ */
151
+ protected static $submitEmptyLogs = false;
152
+
153
+ /**
154
+ * The name of the subject to log.
155
+ *
156
+ * @var string
157
+ */
158
+ protected static $logName = 'maintenance';
159
+
160
+ /**
161
+ * Get the activity log options for the model.
162
+ */
163
+ public function getActivitylogOptions(): LogOptions
164
+ {
165
+ return LogOptions::defaults()->logAll();
166
+ }
167
+
168
+ public function workOrder(): BelongsTo
169
+ {
170
+ return $this->belongsTo(WorkOrder::class, 'work_order_uuid', 'uuid');
171
+ }
172
+
173
+ public function createdBy(): BelongsTo
174
+ {
175
+ return $this->belongsTo(User::class, 'created_by_uuid', 'uuid');
176
+ }
177
+
178
+ public function updatedBy(): BelongsTo
179
+ {
180
+ return $this->belongsTo(User::class, 'updated_by_uuid', 'uuid');
181
+ }
182
+
183
+ public function maintainable(): MorphTo
184
+ {
185
+ return $this->morphTo();
186
+ }
187
+
188
+ public function performedBy(): MorphTo
189
+ {
190
+ return $this->morphTo();
191
+ }
192
+
193
+ /**
194
+ * Get the maintainable entity name.
195
+ */
196
+ public function getMaintainableNameAttribute(): ?string
197
+ {
198
+ if ($this->maintainable) {
199
+ return $this->maintainable->name ?? $this->maintainable->display_name ?? null;
200
+ }
201
+
202
+ return null;
203
+ }
204
+
205
+ /**
206
+ * Get the work order subject.
207
+ */
208
+ public function getWorkOrderSubjectAttribute(): ?string
209
+ {
210
+ return $this->workOrder?->subject;
211
+ }
212
+
213
+ /**
214
+ * Get the name of who performed the maintenance.
215
+ */
216
+ public function getPerformedByNameAttribute(): ?string
217
+ {
218
+ if ($this->performedBy) {
219
+ return $this->performedBy->name ?? $this->performedBy->display_name ?? null;
220
+ }
221
+
222
+ return null;
223
+ }
224
+
225
+ /**
226
+ * Get the duration in hours.
227
+ */
228
+ public function getDurationHoursAttribute(): ?float
229
+ {
230
+ if ($this->started_at && $this->completed_at) {
231
+ return $this->started_at->diffInHours($this->completed_at);
232
+ }
233
+
234
+ return null;
235
+ }
236
+
237
+ /**
238
+ * Check if the maintenance is overdue.
239
+ */
240
+ public function getIsOverdueAttribute(): bool
241
+ {
242
+ if (!$this->scheduled_at || $this->status === 'completed') {
243
+ return false;
244
+ }
245
+
246
+ return now()->gt($this->scheduled_at);
247
+ }
248
+
249
+ /**
250
+ * Get the number of days until due.
251
+ */
252
+ public function getDaysUntilDueAttribute(): ?int
253
+ {
254
+ if (!$this->scheduled_at || $this->status === 'completed') {
255
+ return null;
256
+ }
257
+
258
+ return now()->diffInDays($this->scheduled_at, false);
259
+ }
260
+
261
+ /**
262
+ * Get the cost breakdown.
263
+ */
264
+ public function getCostBreakdownAttribute(): array
265
+ {
266
+ return [
267
+ 'labor_cost' => $this->labor_cost ?? 0,
268
+ 'parts_cost' => $this->parts_cost ?? 0,
269
+ 'tax' => $this->tax ?? 0,
270
+ 'subtotal' => ($this->labor_cost ?? 0) + ($this->parts_cost ?? 0),
271
+ 'total_cost' => $this->total_cost ?? 0,
272
+ ];
273
+ }
274
+
275
+ /**
276
+ * Scope to get maintenance by status.
277
+ *
278
+ * @param \Illuminate\Database\Eloquent\Builder $query
279
+ *
280
+ * @return \Illuminate\Database\Eloquent\Builder
281
+ */
282
+ public function scopeByStatus($query, string $status)
283
+ {
284
+ return $query->where('status', $status);
285
+ }
286
+
287
+ /**
288
+ * Scope to get scheduled maintenance.
289
+ *
290
+ * @param \Illuminate\Database\Eloquent\Builder $query
291
+ *
292
+ * @return \Illuminate\Database\Eloquent\Builder
293
+ */
294
+ public function scopeScheduled($query)
295
+ {
296
+ return $query->where('status', 'scheduled');
297
+ }
298
+
299
+ /**
300
+ * Scope to get overdue maintenance.
301
+ *
302
+ * @param \Illuminate\Database\Eloquent\Builder $query
303
+ *
304
+ * @return \Illuminate\Database\Eloquent\Builder
305
+ */
306
+ public function scopeOverdue($query)
307
+ {
308
+ return $query->where('scheduled_at', '<', now())
309
+ ->where('status', '!=', 'completed');
310
+ }
311
+
312
+ /**
313
+ * Scope to get maintenance by type.
314
+ *
315
+ * @param \Illuminate\Database\Eloquent\Builder $query
316
+ *
317
+ * @return \Illuminate\Database\Eloquent\Builder
318
+ */
319
+ public function scopeByType($query, string $type)
320
+ {
321
+ return $query->where('type', $type);
322
+ }
323
+
324
+ /**
325
+ * Scope to get maintenance by priority.
326
+ *
327
+ * @param \Illuminate\Database\Eloquent\Builder $query
328
+ *
329
+ * @return \Illuminate\Database\Eloquent\Builder
330
+ */
331
+ public function scopeByPriority($query, string $priority)
332
+ {
333
+ return $query->where('priority', $priority);
334
+ }
335
+
336
+ /**
337
+ * Start the maintenance.
338
+ */
339
+ public function start(?Model $performedBy = null): bool
340
+ {
341
+ if ($this->status !== 'scheduled') {
342
+ return false;
343
+ }
344
+
345
+ $updateData = [
346
+ 'status' => 'in_progress',
347
+ 'started_at' => now(),
348
+ ];
349
+
350
+ if ($performedBy) {
351
+ $updateData['performed_by_type'] = get_class($performedBy);
352
+ $updateData['performed_by_uuid'] = $performedBy->uuid;
353
+ }
354
+
355
+ $updated = $this->update($updateData);
356
+
357
+ if ($updated) {
358
+ activity('maintenance_started')
359
+ ->performedOn($this)
360
+ ->withProperties([
361
+ 'performed_by_type' => $updateData['performed_by_type'] ?? null,
362
+ 'performed_by_uuid' => $updateData['performed_by_uuid'] ?? null,
363
+ ])
364
+ ->log('Maintenance started');
365
+ }
366
+
367
+ return $updated;
368
+ }
369
+
370
+ /**
371
+ * Complete the maintenance.
372
+ */
373
+ public function complete(array $completionData = []): bool
374
+ {
375
+ if (!in_array($this->status, ['scheduled', 'in_progress'])) {
376
+ return false;
377
+ }
378
+
379
+ $updateData = [
380
+ 'status' => 'completed',
381
+ 'completed_at' => now(),
382
+ ];
383
+
384
+ // Update costs if provided
385
+ if (isset($completionData['labor_cost'])) {
386
+ $updateData['labor_cost'] = $completionData['labor_cost'];
387
+ }
388
+ if (isset($completionData['parts_cost'])) {
389
+ $updateData['parts_cost'] = $completionData['parts_cost'];
390
+ }
391
+ if (isset($completionData['tax'])) {
392
+ $updateData['tax'] = $completionData['tax'];
393
+ }
394
+
395
+ // Calculate total cost
396
+ $laborCost = $updateData['labor_cost'] ?? $this->labor_cost ?? 0;
397
+ $partsCost = $updateData['parts_cost'] ?? $this->parts_cost ?? 0;
398
+ $tax = $updateData['tax'] ?? $this->tax ?? 0;
399
+ $updateData['total_cost'] = $laborCost + $partsCost + $tax;
400
+
401
+ // Update other fields
402
+ if (isset($completionData['notes'])) {
403
+ $updateData['notes'] = $completionData['notes'];
404
+ }
405
+ if (isset($completionData['line_items'])) {
406
+ $updateData['line_items'] = $completionData['line_items'];
407
+ }
408
+ if (isset($completionData['attachments'])) {
409
+ $updateData['attachments'] = $completionData['attachments'];
410
+ }
411
+
412
+ $updated = $this->update($updateData);
413
+
414
+ if ($updated) {
415
+ activity('maintenance_completed')
416
+ ->performedOn($this)
417
+ ->withProperties($completionData)
418
+ ->log('Maintenance completed');
419
+
420
+ // Update the maintainable entity's odometer/engine hours if provided
421
+ if ($this->maintainable && isset($completionData['odometer'])) {
422
+ $this->maintainable->updateOdometer($completionData['odometer'], 'maintenance');
423
+ }
424
+ if ($this->maintainable && isset($completionData['engine_hours'])) {
425
+ $this->maintainable->updateEngineHours($completionData['engine_hours'], 'maintenance');
426
+ }
427
+ }
428
+
429
+ return $updated;
430
+ }
431
+
432
+ /**
433
+ * Cancel the maintenance.
434
+ */
435
+ public function cancel(?string $reason = null): bool
436
+ {
437
+ if ($this->status === 'completed') {
438
+ return false;
439
+ }
440
+
441
+ $updateData = ['status' => 'canceled'];
442
+
443
+ if ($reason) {
444
+ $meta = $this->meta ?? [];
445
+ $meta['cancellation_reason'] = $reason;
446
+ $updateData['meta'] = $meta;
447
+ }
448
+
449
+ $updated = $this->update($updateData);
450
+
451
+ if ($updated) {
452
+ activity('maintenance_canceled')
453
+ ->performedOn($this)
454
+ ->withProperties(['reason' => $reason])
455
+ ->log('Maintenance canceled');
456
+ }
457
+
458
+ return $updated;
459
+ }
460
+
461
+ /**
462
+ * Add a line item to the maintenance.
463
+ */
464
+ public function addLineItem(array $item): bool
465
+ {
466
+ $lineItems = $this->line_items ?? [];
467
+ $lineItems[] = array_merge($item, [
468
+ 'added_at' => now(),
469
+ ]);
470
+
471
+ return $this->update(['line_items' => $lineItems]);
472
+ }
473
+
474
+ /**
475
+ * Remove a line item from the maintenance.
476
+ */
477
+ public function removeLineItem(int $index): bool
478
+ {
479
+ $lineItems = $this->line_items ?? [];
480
+
481
+ if (!isset($lineItems[$index])) {
482
+ return false;
483
+ }
484
+
485
+ unset($lineItems[$index]);
486
+ $lineItems = array_values($lineItems); // Re-index array
487
+
488
+ return $this->update(['line_items' => $lineItems]);
489
+ }
490
+
491
+ /**
492
+ * Add an attachment to the maintenance.
493
+ */
494
+ public function addAttachment(string $fileUuid, ?string $description = null): bool
495
+ {
496
+ $attachments = $this->attachments ?? [];
497
+ $attachments[] = [
498
+ 'file_uuid' => $fileUuid,
499
+ 'description' => $description,
500
+ 'added_at' => now(),
501
+ ];
502
+
503
+ return $this->update(['attachments' => $attachments]);
504
+ }
505
+
506
+ /**
507
+ * Get the efficiency rating based on scheduled vs actual time.
508
+ */
509
+ public function getEfficiencyRating(): ?float
510
+ {
511
+ if (!$this->duration_hours || !$this->scheduled_at || !$this->completed_at) {
512
+ return null;
513
+ }
514
+
515
+ $meta = $this->meta ?? [];
516
+ $estimatedHours = $meta['estimated_duration_hours'] ?? null;
517
+
518
+ if (!$estimatedHours) {
519
+ return null;
520
+ }
521
+
522
+ // Efficiency = estimated / actual (higher is better)
523
+ return min(($estimatedHours / $this->duration_hours) * 100, 100);
524
+ }
525
+
526
+ /**
527
+ * Check if the maintenance was completed on time.
528
+ */
529
+ public function wasCompletedOnTime(): ?bool
530
+ {
531
+ if (!$this->scheduled_at || !$this->completed_at) {
532
+ return null;
533
+ }
534
+
535
+ return $this->completed_at->lte($this->scheduled_at);
536
+ }
537
+
538
+ /**
539
+ * Get the cost per hour of maintenance.
540
+ */
541
+ public function getCostPerHour(): ?float
542
+ {
543
+ if (!$this->duration_hours || !$this->total_cost) {
544
+ return null;
545
+ }
546
+
547
+ return $this->total_cost / $this->duration_hours;
548
+ }
549
+ }