@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,532 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Models;
4
+
5
+ use Fleetbase\Casts\Json;
6
+ use Fleetbase\Models\File;
7
+ use Fleetbase\Models\Model;
8
+ use Fleetbase\Models\User;
9
+ use Fleetbase\Traits\HasApiModelBehavior;
10
+ use Fleetbase\Traits\HasCustomFields;
11
+ use Fleetbase\Traits\HasMetaAttributes;
12
+ use Fleetbase\Traits\HasPublicId;
13
+ use Fleetbase\Traits\HasUuid;
14
+ use Fleetbase\Traits\Searchable;
15
+ use Fleetbase\Traits\TracksApiCredential;
16
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
17
+ use Illuminate\Database\Eloquent\Relations\HasMany;
18
+ use Illuminate\Database\Eloquent\Relations\MorphTo;
19
+ use Spatie\Activitylog\LogOptions;
20
+ use Spatie\Activitylog\Traits\LogsActivity;
21
+
22
+ /**
23
+ * Class WorkOrder.
24
+ *
25
+ * Represents an operational task wrapper that coordinates who, when, and what
26
+ * for maintenance or other jobs in the fleet management system.
27
+ */
28
+ class WorkOrder extends Model
29
+ {
30
+ use HasUuid;
31
+ use HasPublicId;
32
+ use TracksApiCredential;
33
+ use HasApiModelBehavior;
34
+ use LogsActivity;
35
+ use HasMetaAttributes;
36
+ use Searchable;
37
+ use HasCustomFields;
38
+
39
+ /**
40
+ * The database table used by the model.
41
+ *
42
+ * @var string
43
+ */
44
+ protected $table = 'work_orders';
45
+
46
+ /**
47
+ * The type of public Id to generate.
48
+ *
49
+ * @var string
50
+ */
51
+ protected $publicIdType = 'work_order';
52
+
53
+ /**
54
+ * The attributes that can be queried.
55
+ *
56
+ * @var array
57
+ */
58
+ protected $searchableColumns = ['code', 'subject', 'instructions', 'public_id'];
59
+
60
+ /**
61
+ * The attributes that can be used for filtering.
62
+ *
63
+ * @var array
64
+ */
65
+ protected $filterParams = ['status', 'priority', 'target_type', 'assignee_type'];
66
+
67
+ /**
68
+ * The attributes that are mass assignable.
69
+ *
70
+ * @var array
71
+ */
72
+ protected $fillable = [
73
+ 'company_uuid',
74
+ 'code',
75
+ 'subject',
76
+ 'status',
77
+ 'priority',
78
+ 'target_type',
79
+ 'target_uuid',
80
+ 'assignee_type',
81
+ 'assignee_uuid',
82
+ 'opened_at',
83
+ 'due_at',
84
+ 'closed_at',
85
+ 'instructions',
86
+ 'checklist',
87
+ 'meta',
88
+ 'slug',
89
+ ];
90
+
91
+ /**
92
+ * Dynamic attributes that are appended to object.
93
+ *
94
+ * @var array
95
+ */
96
+ protected $appends = [
97
+ 'target_name',
98
+ 'assignee_name',
99
+ 'is_overdue',
100
+ 'days_until_due',
101
+ 'completion_percentage',
102
+ 'estimated_duration',
103
+ ];
104
+
105
+ /**
106
+ * The attributes excluded from the model's JSON form.
107
+ *
108
+ * @var array
109
+ */
110
+ protected $hidden = ['target', 'assignee'];
111
+
112
+ /**
113
+ * The attributes that should be cast to native types.
114
+ *
115
+ * @var array
116
+ */
117
+ protected $casts = [
118
+ 'opened_at' => 'datetime',
119
+ 'due_at' => 'datetime',
120
+ 'closed_at' => 'datetime',
121
+ 'checklist' => Json::class,
122
+ 'meta' => Json::class,
123
+ ];
124
+
125
+ /**
126
+ * Properties which activity needs to be logged.
127
+ *
128
+ * @var array
129
+ */
130
+ protected static $logAttributes = '*';
131
+
132
+ /**
133
+ * Do not log empty changed.
134
+ *
135
+ * @var bool
136
+ */
137
+ protected static $submitEmptyLogs = false;
138
+
139
+ /**
140
+ * The name of the subject to log.
141
+ *
142
+ * @var string
143
+ */
144
+ protected static $logName = 'work_order';
145
+
146
+ /**
147
+ * Get the activity log options for the model.
148
+ */
149
+ public function getActivitylogOptions(): LogOptions
150
+ {
151
+ return LogOptions::defaults()->logAll();
152
+ }
153
+
154
+ public function createdBy(): BelongsTo
155
+ {
156
+ return $this->belongsTo(User::class, 'created_by_uuid', 'uuid');
157
+ }
158
+
159
+ public function updatedBy(): BelongsTo
160
+ {
161
+ return $this->belongsTo(User::class, 'updated_by_uuid', 'uuid');
162
+ }
163
+
164
+ public function target(): MorphTo
165
+ {
166
+ return $this->morphTo();
167
+ }
168
+
169
+ public function assignee(): MorphTo
170
+ {
171
+ return $this->morphTo();
172
+ }
173
+
174
+ public function maintenances(): HasMany
175
+ {
176
+ return $this->hasMany(Maintenance::class, 'work_order_uuid', 'uuid');
177
+ }
178
+
179
+ public function documents(): HasMany
180
+ {
181
+ return $this->hasMany(File::class, 'subject_uuid', 'uuid');
182
+ }
183
+
184
+ /**
185
+ * Get the target name.
186
+ */
187
+ public function getTargetNameAttribute(): ?string
188
+ {
189
+ if ($this->target) {
190
+ return $this->target->name ?? $this->target->display_name ?? null;
191
+ }
192
+
193
+ return null;
194
+ }
195
+
196
+ /**
197
+ * Get the assignee name.
198
+ */
199
+ public function getAssigneeNameAttribute(): ?string
200
+ {
201
+ if ($this->assignee) {
202
+ return $this->assignee->name ?? $this->assignee->display_name ?? null;
203
+ }
204
+
205
+ return null;
206
+ }
207
+
208
+ /**
209
+ * Check if the work order is overdue.
210
+ */
211
+ public function getIsOverdueAttribute(): bool
212
+ {
213
+ if (!$this->due_at || $this->status === 'closed') {
214
+ return false;
215
+ }
216
+
217
+ return now()->gt($this->due_at);
218
+ }
219
+
220
+ /**
221
+ * Get the number of days until due.
222
+ */
223
+ public function getDaysUntilDueAttribute(): ?int
224
+ {
225
+ if (!$this->due_at || $this->status === 'closed') {
226
+ return null;
227
+ }
228
+
229
+ $days = now()->diffInDays($this->due_at, false);
230
+
231
+ return $days;
232
+ }
233
+
234
+ /**
235
+ * Get the completion percentage based on checklist.
236
+ */
237
+ public function getCompletionPercentageAttribute(): float
238
+ {
239
+ $checklist = $this->checklist ?? [];
240
+
241
+ if (empty($checklist)) {
242
+ return $this->status === 'closed' ? 100.0 : 0.0;
243
+ }
244
+
245
+ $totalItems = count($checklist);
246
+ $completedItems = 0;
247
+
248
+ foreach ($checklist as $item) {
249
+ if (isset($item['completed']) && $item['completed']) {
250
+ $completedItems++;
251
+ }
252
+ }
253
+
254
+ return $totalItems > 0 ? ($completedItems / $totalItems) * 100 : 0.0;
255
+ }
256
+
257
+ /**
258
+ * Get the estimated duration in hours.
259
+ */
260
+ public function getEstimatedDurationAttribute(): ?float
261
+ {
262
+ $meta = $this->meta ?? [];
263
+
264
+ return $meta['estimated_duration_hours'] ?? null;
265
+ }
266
+
267
+ /**
268
+ * Scope to get work orders by status.
269
+ *
270
+ * @param \Illuminate\Database\Eloquent\Builder $query
271
+ *
272
+ * @return \Illuminate\Database\Eloquent\Builder
273
+ */
274
+ public function scopeByStatus($query, string $status)
275
+ {
276
+ return $query->where('status', $status);
277
+ }
278
+
279
+ /**
280
+ * Scope to get open work orders.
281
+ *
282
+ * @param \Illuminate\Database\Eloquent\Builder $query
283
+ *
284
+ * @return \Illuminate\Database\Eloquent\Builder
285
+ */
286
+ public function scopeOpen($query)
287
+ {
288
+ return $query->whereIn('status', ['open', 'in_progress']);
289
+ }
290
+
291
+ /**
292
+ * Scope to get overdue work orders.
293
+ *
294
+ * @param \Illuminate\Database\Eloquent\Builder $query
295
+ *
296
+ * @return \Illuminate\Database\Eloquent\Builder
297
+ */
298
+ public function scopeOverdue($query)
299
+ {
300
+ return $query->where('due_at', '<', now())
301
+ ->whereNotIn('status', ['closed', 'canceled']);
302
+ }
303
+
304
+ /**
305
+ * Scope to get work orders by priority.
306
+ *
307
+ * @param \Illuminate\Database\Eloquent\Builder $query
308
+ *
309
+ * @return \Illuminate\Database\Eloquent\Builder
310
+ */
311
+ public function scopeByPriority($query, string $priority)
312
+ {
313
+ return $query->where('priority', $priority);
314
+ }
315
+
316
+ /**
317
+ * Scope to get work orders assigned to a specific entity.
318
+ *
319
+ * @param \Illuminate\Database\Eloquent\Builder $query
320
+ *
321
+ * @return \Illuminate\Database\Eloquent\Builder
322
+ */
323
+ public function scopeAssignedTo($query, string $type, string $uuid)
324
+ {
325
+ return $query->where('assignee_type', $type)
326
+ ->where('assignee_uuid', $uuid);
327
+ }
328
+
329
+ /**
330
+ * Assign the work order to an entity.
331
+ */
332
+ public function assignTo(Model $assignee): bool
333
+ {
334
+ $updated = $this->update([
335
+ 'assignee_type' => get_class($assignee),
336
+ 'assignee_uuid' => $assignee->uuid,
337
+ ]);
338
+
339
+ if ($updated) {
340
+ activity('work_order_assigned')
341
+ ->performedOn($this)
342
+ ->withProperties([
343
+ 'assigned_to_type' => get_class($assignee),
344
+ 'assigned_to_uuid' => $assignee->uuid,
345
+ 'assigned_to_name' => $assignee->name ?? $assignee->display_name ?? null,
346
+ ])
347
+ ->log('Work order assigned');
348
+ }
349
+
350
+ return $updated;
351
+ }
352
+
353
+ /**
354
+ * Start the work order.
355
+ */
356
+ public function start(): bool
357
+ {
358
+ if ($this->status !== 'open') {
359
+ return false;
360
+ }
361
+
362
+ $updated = $this->update([
363
+ 'status' => 'in_progress',
364
+ 'opened_at' => $this->opened_at ?? now(),
365
+ ]);
366
+
367
+ if ($updated) {
368
+ activity('work_order_started')
369
+ ->performedOn($this)
370
+ ->log('Work order started');
371
+ }
372
+
373
+ return $updated;
374
+ }
375
+
376
+ /**
377
+ * Complete the work order.
378
+ */
379
+ public function complete(array $completionData = []): bool
380
+ {
381
+ if (!in_array($this->status, ['open', 'in_progress'])) {
382
+ return false;
383
+ }
384
+
385
+ $updateData = [
386
+ 'status' => 'closed',
387
+ 'closed_at' => now(),
388
+ ];
389
+
390
+ // Update meta with completion data
391
+ if (!empty($completionData)) {
392
+ $meta = $this->meta ?? [];
393
+ $meta['completion_data'] = $completionData;
394
+ $updateData['meta'] = $meta;
395
+ }
396
+
397
+ $updated = $this->update($updateData);
398
+
399
+ if ($updated) {
400
+ activity('work_order_completed')
401
+ ->performedOn($this)
402
+ ->withProperties($completionData)
403
+ ->log('Work order completed');
404
+ }
405
+
406
+ return $updated;
407
+ }
408
+
409
+ /**
410
+ * Cancel the work order.
411
+ */
412
+ public function cancel(?string $reason = null): bool
413
+ {
414
+ if ($this->status === 'closed') {
415
+ return false;
416
+ }
417
+
418
+ $updateData = [
419
+ 'status' => 'canceled',
420
+ 'closed_at' => now(),
421
+ ];
422
+
423
+ if ($reason) {
424
+ $meta = $this->meta ?? [];
425
+ $meta['cancellation_reason'] = $reason;
426
+ $updateData['meta'] = $meta;
427
+ }
428
+
429
+ $updated = $this->update($updateData);
430
+
431
+ if ($updated) {
432
+ activity('work_order_canceled')
433
+ ->performedOn($this)
434
+ ->withProperties(['reason' => $reason])
435
+ ->log('Work order canceled');
436
+ }
437
+
438
+ return $updated;
439
+ }
440
+
441
+ /**
442
+ * Update a checklist item.
443
+ */
444
+ public function updateChecklistItem(int $itemIndex, array $itemData): bool
445
+ {
446
+ $checklist = $this->checklist ?? [];
447
+
448
+ if (!isset($checklist[$itemIndex])) {
449
+ return false;
450
+ }
451
+
452
+ $checklist[$itemIndex] = array_merge($checklist[$itemIndex], $itemData);
453
+
454
+ return $this->update(['checklist' => $checklist]);
455
+ }
456
+
457
+ /**
458
+ * Mark a checklist item as completed.
459
+ */
460
+ public function completeChecklistItem(int $itemIndex, ?string $completedBy = null): bool
461
+ {
462
+ return $this->updateChecklistItem($itemIndex, [
463
+ 'completed' => true,
464
+ 'completed_at' => now(),
465
+ 'completed_by' => $completedBy ?? auth()->id(),
466
+ ]);
467
+ }
468
+
469
+ /**
470
+ * Add a new checklist item.
471
+ */
472
+ public function addChecklistItem(array $item): bool
473
+ {
474
+ $checklist = $this->checklist ?? [];
475
+ $checklist[] = array_merge($item, [
476
+ 'completed' => false,
477
+ 'created_at' => now(),
478
+ ]);
479
+
480
+ return $this->update(['checklist' => $checklist]);
481
+ }
482
+
483
+ /**
484
+ * Get the actual duration of the work order.
485
+ *
486
+ * @return float|null Hours
487
+ */
488
+ public function getActualDuration(): ?float
489
+ {
490
+ if (!$this->opened_at || !$this->closed_at) {
491
+ return null;
492
+ }
493
+
494
+ return $this->opened_at->diffInHours($this->closed_at);
495
+ }
496
+
497
+ /**
498
+ * Check if the work order is on schedule.
499
+ */
500
+ public function isOnSchedule(): ?bool
501
+ {
502
+ if (!$this->due_at) {
503
+ return null;
504
+ }
505
+
506
+ if ($this->status === 'closed') {
507
+ return $this->closed_at->lte($this->due_at);
508
+ }
509
+
510
+ // For open work orders, check if we're still within the due date
511
+ return now()->lte($this->due_at);
512
+ }
513
+
514
+ /**
515
+ * Get the priority level as a numeric value for sorting.
516
+ */
517
+ public function getPriorityLevel(): int
518
+ {
519
+ switch ($this->priority) {
520
+ case 'critical':
521
+ return 5;
522
+ case 'high':
523
+ return 4;
524
+ case 'medium':
525
+ return 3;
526
+ case 'low':
527
+ return 2;
528
+ default:
529
+ return 1;
530
+ }
531
+ }
532
+ }
@@ -200,6 +200,11 @@ class Utils extends FleetbaseUtils
200
200
  $coordinates = $coordinates->location;
201
201
  }
202
202
 
203
+ // any model with spatial location point
204
+ if ($coordinates instanceof \Illuminate\Database\Eloquent\Model && $coordinates->location) {
205
+ $coordinates = $coordinates->location;
206
+ }
207
+
203
208
  if ($coordinates instanceof \Fleetbase\LaravelMysqlSpatial\Eloquent\SpatialExpression) {
204
209
  $coordinates = $coordinates->getSpatialValue();
205
210
  }
@@ -3,13 +3,16 @@
3
3
  namespace Fleetbase\FleetOps\Traits;
4
4
 
5
5
  use Fleetbase\FleetOps\Flow\Activity;
6
+ use Fleetbase\FleetOps\Models\Order;
6
7
  use Fleetbase\FleetOps\Models\Proof;
7
8
  use Fleetbase\FleetOps\Models\TrackingNumber;
8
9
  use Fleetbase\FleetOps\Models\TrackingStatus;
9
10
  use Fleetbase\FleetOps\Support\Utils;
10
11
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
12
+ use Fleetbase\Support\TemplateString;
11
13
  use Illuminate\Database\Eloquent\Model;
12
14
  use Illuminate\Support\Facades\DB;
15
+ use Illuminate\Support\Facades\Log;
13
16
 
14
17
  trait HasTrackingNumber
15
18
  {
@@ -55,11 +58,11 @@ trait HasTrackingNumber
55
58
  */
56
59
  public function createActivity(Activity $activity, $location = [], $proof = null): TrackingStatus
57
60
  {
58
- $status = $activity->get('status');
59
- $details = $activity->get('details');
60
- $code = $activity->get('code');
61
- $proof = static::resolveProof($proof);
62
- $activity = TrackingStatus::create([
61
+ $status = $this->resolveActivityTemplateString($activity->get('status', ''));
62
+ $details = $this->resolveActivityTemplateString($activity->get('details', ''));
63
+ $code = $activity->get('code');
64
+ $proof = static::resolveProof($proof);
65
+ $activity = TrackingStatus::create([
63
66
  'company_uuid' => data_get($this, 'company_uuid', session('company')),
64
67
  'tracking_number_uuid' => $this->tracking_number_uuid,
65
68
  'proof_uuid' => data_get($proof, 'uuid'),
@@ -89,11 +92,11 @@ trait HasTrackingNumber
89
92
  */
90
93
  public function insertActivity(Activity $activity, $location = [], $proof = null): string
91
94
  {
92
- $status = $activity->get('status');
93
- $details = $activity->get('details');
94
- $code = $activity->get('code');
95
- $proof = static::resolveProof($proof);
96
- $activityId = TrackingStatus::insertGetUuid([
95
+ $status = $this->resolveActivityTemplateString($activity->get('status', ''));
96
+ $details = $this->resolveActivityTemplateString($activity->get('details', ''));
97
+ $code = $activity->get('code');
98
+ $proof = static::resolveProof($proof);
99
+ $activityId = TrackingStatus::insertGetUuid([
97
100
  'company_uuid' => data_get($this, 'company_uuid', session('company')),
98
101
  'tracking_number_uuid' => $this->tracking_number_uuid,
99
102
  'proof_uuid' => data_get($proof, 'uuid'),
@@ -184,4 +187,55 @@ trait HasTrackingNumber
184
187
 
185
188
  return null;
186
189
  }
190
+
191
+ /**
192
+ * Resolve {placeholders} inside an activity template string.
193
+ *
194
+ * Behavior:
195
+ * - If called on an Order, resolves placeholders against that Order.
196
+ * - Otherwise, if the model exposes getOrder(), resolves against the returned Order.
197
+ * - If no suitable target model can be determined, returns the original template unchanged.
198
+ * - Uses App\Support\TemplateString::resolve() to process placeholders and modifiers
199
+ * (e.g., {waypoint.type}, {capitalize waypoint.type}, {order.number | snake | uppercase}).
200
+ *
201
+ * Robustness:
202
+ * - Fast path: if there are no braces, returns early.
203
+ * - Catches unexpected resolver failures and logs a warning rather than throwing inside UI flows.
204
+ *
205
+ * @param string $template the template containing {placeholders}
206
+ *
207
+ * @return string the resolved template string, or the original on fallback
208
+ */
209
+ private function resolveActivityTemplateString(string $template): string
210
+ {
211
+ // Fast path: no placeholders to resolve.
212
+ if ($template === '' || strpos($template, '{') === false) {
213
+ return $template;
214
+ }
215
+
216
+ // Determine which model should resolve dynamic properties.
217
+ // Prefer $this when it's already an Order; otherwise try a getOrder() accessor.
218
+ $target = $this instanceof Order
219
+ ? $this
220
+ : (method_exists($this, 'getOrder') ? $this->getOrder() : null);
221
+
222
+ // If we couldn't locate a suitable Eloquent model, leave template unchanged.
223
+ if (!$target instanceof Model) {
224
+ return $template;
225
+ }
226
+
227
+ try {
228
+ // Uses default resolver name 'resolveDynamicProperty' on the target model.
229
+ return TemplateString::resolve($template, $target, 'resolveDynamicProperty');
230
+ } catch (\Throwable $e) {
231
+ // Don't break user flows on template issues; log and return original.
232
+ Log::warning('Activity template resolution failed.', [
233
+ 'template' => $template,
234
+ 'target' => get_class($target),
235
+ 'message' => $e->getMessage(),
236
+ ]);
237
+
238
+ return $template;
239
+ }
240
+ }
187
241
  }