@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.
- package/addon/components/layout/fleet-ops-sidebar.hbs +25 -0
- package/addon/templates/virtual.hbs +3 -3
- package/composer.json +3 -2
- package/extension.json +1 -1
- package/package.json +1 -1
- package/server/migrations/2025_08_28_054920_create_warranties_table.php +56 -0
- package/server/migrations/2025_08_28_054921_create_telematics_table.php +60 -0
- package/server/migrations/2025_08_28_054922_create_assets_table.php +94 -0
- package/server/migrations/2025_08_28_054925_create_devices_table.php +190 -0
- package/server/migrations/2025_08_28_054926_create_device_events_table.php +99 -0
- package/server/migrations/2025_08_28_054926_create_sensors_table.php +62 -0
- package/server/migrations/2025_08_28_054927_create_parts_table.php +73 -0
- package/server/migrations/2025_08_28_054929_create_equipments_table.php +58 -0
- package/server/migrations/2025_08_28_054930_create_work_orders_table.php +85 -0
- package/server/migrations/2025_08_28_054931_create_maintenances_table.php +79 -0
- package/server/migrations/2025_08_28_082002_update_vehicles_table_telematics.php +60 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -1
- package/server/src/Http/Resources/v1/Order.php +111 -60
- package/server/src/Models/Asset.php +548 -0
- package/server/src/Models/Contact.php +2 -0
- package/server/src/Models/Device.php +435 -0
- package/server/src/Models/DeviceEvent.php +501 -0
- package/server/src/Models/Driver.php +2 -0
- package/server/src/Models/Entity.php +2 -0
- package/server/src/Models/Equipment.php +483 -0
- package/server/src/Models/Fleet.php +2 -0
- package/server/src/Models/FuelReport.php +2 -0
- package/server/src/Models/Issue.php +2 -0
- package/server/src/Models/Maintenance.php +549 -0
- package/server/src/Models/Order.php +32 -112
- package/server/src/Models/OrderConfig.php +8 -0
- package/server/src/Models/Part.php +502 -0
- package/server/src/Models/Payload.php +101 -20
- package/server/src/Models/Place.php +10 -4
- package/server/src/Models/Sensor.php +510 -0
- package/server/src/Models/ServiceArea.php +1 -1
- package/server/src/Models/Telematic.php +336 -0
- package/server/src/Models/Vehicle.php +45 -1
- package/server/src/Models/VehicleDevice.php +1 -1
- package/server/src/Models/Vendor.php +2 -0
- package/server/src/Models/Warranty.php +413 -0
- package/server/src/Models/WorkOrder.php +532 -0
- package/server/src/Support/Utils.php +5 -0
- 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
|
+
}
|