@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
|
@@ -14,11 +14,10 @@ use Fleetbase\FleetOps\Support\OrderTracker;
|
|
|
14
14
|
use Fleetbase\FleetOps\Support\Utils;
|
|
15
15
|
use Fleetbase\FleetOps\Traits\HasTrackingNumber;
|
|
16
16
|
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
17
|
-
use Fleetbase\Models\CustomField;
|
|
18
|
-
use Fleetbase\Models\CustomFieldValue;
|
|
19
17
|
use Fleetbase\Models\Model;
|
|
20
18
|
use Fleetbase\Models\Transaction;
|
|
21
19
|
use Fleetbase\Traits\HasApiModelBehavior;
|
|
20
|
+
use Fleetbase\Traits\HasCustomFields;
|
|
22
21
|
use Fleetbase\Traits\HasInternalId;
|
|
23
22
|
use Fleetbase\Traits\HasMetaAttributes;
|
|
24
23
|
use Fleetbase\Traits\HasOptionsAttributes;
|
|
@@ -34,6 +33,7 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
|
|
34
33
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
35
34
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
|
36
35
|
use Illuminate\Support\Carbon;
|
|
36
|
+
use Illuminate\Support\Collection;
|
|
37
37
|
use Illuminate\Support\Str;
|
|
38
38
|
use Spatie\Activitylog\LogOptions;
|
|
39
39
|
use Spatie\Activitylog\Traits\LogsActivity;
|
|
@@ -51,6 +51,7 @@ class Order extends Model
|
|
|
51
51
|
use Searchable;
|
|
52
52
|
use LogsActivity;
|
|
53
53
|
use HasTrackingNumber;
|
|
54
|
+
use HasCustomFields;
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
57
|
* The database table used by the model.
|
|
@@ -348,16 +349,6 @@ class Order extends Model
|
|
|
348
349
|
return $this->hasMany(\Fleetbase\Models\File::class, 'subject_uuid')->latest();
|
|
349
350
|
}
|
|
350
351
|
|
|
351
|
-
public function customFields(): HasMany
|
|
352
|
-
{
|
|
353
|
-
return $this->hasMany(CustomField::class, 'subject_uuid')->orderBy('order');
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
public function customFieldValues(): HasMany
|
|
357
|
-
{
|
|
358
|
-
return $this->hasMany(CustomFieldValue::class, 'subject_uuid');
|
|
359
|
-
}
|
|
360
|
-
|
|
361
352
|
public function drivers(): HasManyThrough
|
|
362
353
|
{
|
|
363
354
|
return $this->hasManyThrough(Driver::class, Entity::class, 'tracking_number_uuid', 'tracking_number_uuid');
|
|
@@ -1141,7 +1132,7 @@ class Order extends Model
|
|
|
1141
1132
|
*/
|
|
1142
1133
|
public function firstDispatch(): Order
|
|
1143
1134
|
{
|
|
1144
|
-
if (
|
|
1135
|
+
if (!$this->dispatched) {
|
|
1145
1136
|
$this->dispatch();
|
|
1146
1137
|
}
|
|
1147
1138
|
|
|
@@ -1156,7 +1147,7 @@ class Order extends Model
|
|
|
1156
1147
|
*/
|
|
1157
1148
|
public function firstDispatchWithActivity(): Order
|
|
1158
1149
|
{
|
|
1159
|
-
if (
|
|
1150
|
+
if (!$this->dispatched) {
|
|
1160
1151
|
$this->dispatchWithActivity();
|
|
1161
1152
|
}
|
|
1162
1153
|
|
|
@@ -1603,104 +1594,6 @@ class Order extends Model
|
|
|
1603
1594
|
return (int) Utils::get($this, 'adhoc_distance', Utils::get($this, 'company.options.fleetops.adhoc_distance', 6000));
|
|
1604
1595
|
}
|
|
1605
1596
|
|
|
1606
|
-
/**
|
|
1607
|
-
* Retrieves a custom field by its key.
|
|
1608
|
-
*
|
|
1609
|
-
* This method searches for a custom field where the name or label matches the given key.
|
|
1610
|
-
*
|
|
1611
|
-
* @param string $key the key used to search for the custom field
|
|
1612
|
-
*
|
|
1613
|
-
* @return CustomField|null the found CustomField object or null if not found
|
|
1614
|
-
*/
|
|
1615
|
-
public function getCustomField(string $key): ?CustomField
|
|
1616
|
-
{
|
|
1617
|
-
$name = Str::slug($key);
|
|
1618
|
-
$label = Str::title($key);
|
|
1619
|
-
|
|
1620
|
-
return $this->customFields()->where('name', $name)->orWhere('label', $label)->first();
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
/**
|
|
1624
|
-
* Retrieves the custom field value for the specified custom field.
|
|
1625
|
-
*
|
|
1626
|
-
* @param CustomField $customField the custom field to retrieve the value for
|
|
1627
|
-
*
|
|
1628
|
-
* @return CustomFieldValue|null the custom field value, or null if not found
|
|
1629
|
-
*/
|
|
1630
|
-
public function getCustomFieldValue(CustomField $customField): ?CustomFieldValue
|
|
1631
|
-
{
|
|
1632
|
-
$customFieldValue = $this->customFieldValues()->where('custom_field_uuid', $customField->uuid)->first();
|
|
1633
|
-
if ($customFieldValue) {
|
|
1634
|
-
return $customFieldValue;
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
return null;
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
/**
|
|
1641
|
-
* Retrieves the value of a custom field by its key.
|
|
1642
|
-
*
|
|
1643
|
-
* @param string $key the key of the custom field
|
|
1644
|
-
*
|
|
1645
|
-
* @return mixed|null the value of the custom field, or null if not found
|
|
1646
|
-
*/
|
|
1647
|
-
public function getCustomFieldValueByKey(string $key)
|
|
1648
|
-
{
|
|
1649
|
-
$customField = $this->getCustomField($key);
|
|
1650
|
-
if ($customField) {
|
|
1651
|
-
$customFieldValue = $this->getCustomFieldValue($customField);
|
|
1652
|
-
if ($customFieldValue) {
|
|
1653
|
-
return $customFieldValue->value;
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
return null;
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
/**
|
|
1661
|
-
* Checks if a custom field exists.
|
|
1662
|
-
*
|
|
1663
|
-
* @param string $key the key of the custom field
|
|
1664
|
-
*/
|
|
1665
|
-
public function isCustomField(string $key): bool
|
|
1666
|
-
{
|
|
1667
|
-
$name = Str::slug($key);
|
|
1668
|
-
$label = Str::title($key);
|
|
1669
|
-
|
|
1670
|
-
return $this->customFields()->where('name', $name)->orWhere('label', $label)->exists();
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
/**
|
|
1674
|
-
* Retrieves all custom field values associated with the order.
|
|
1675
|
-
*
|
|
1676
|
-
* @return array an array of custom field values
|
|
1677
|
-
*/
|
|
1678
|
-
public function getCustomFieldValues(): array
|
|
1679
|
-
{
|
|
1680
|
-
$customFields = [];
|
|
1681
|
-
foreach ($this->customFieldValues as $customFieldValue) {
|
|
1682
|
-
$key = Str::snake(strtolower($customFieldValue->custom_field_label));
|
|
1683
|
-
if ($key) {
|
|
1684
|
-
$customFields[$key] = $customFieldValue->value;
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
return $customFields;
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
public function getCustomFieldKeys(): array
|
|
1692
|
-
{
|
|
1693
|
-
$keys = [];
|
|
1694
|
-
foreach ($this->customFieldValues as $customFieldValue) {
|
|
1695
|
-
$key = Str::snake(strtolower($customFieldValue->custom_field_label));
|
|
1696
|
-
if ($key) {
|
|
1697
|
-
$keys[] = $key;
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
return $keys;
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
1597
|
/**
|
|
1705
1598
|
* Retrieves the OrderConfig associated with this order.
|
|
1706
1599
|
*
|
|
@@ -1925,4 +1818,31 @@ class Order extends Model
|
|
|
1925
1818
|
|
|
1926
1819
|
return $this->{$property};
|
|
1927
1820
|
}
|
|
1821
|
+
|
|
1822
|
+
public function findClosestDrivers(int $distance = 6000): Collection
|
|
1823
|
+
{
|
|
1824
|
+
$pickup = $this->getPickupLocation();
|
|
1825
|
+
$distance = $distance;
|
|
1826
|
+
|
|
1827
|
+
if (!Utils::isPoint($pickup)) {
|
|
1828
|
+
return collect();
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
$drivers = Driver::where(['status' => 'active', 'online' => 1])
|
|
1832
|
+
->whereHas('company', function ($q) {
|
|
1833
|
+
$q->whereHas('users', function ($q) {
|
|
1834
|
+
$q->whereHas('driver', function ($q) {
|
|
1835
|
+
$q->where(['status' => 'active', 'online' => 1]);
|
|
1836
|
+
$q->whereNull('deleted_at');
|
|
1837
|
+
});
|
|
1838
|
+
});
|
|
1839
|
+
})
|
|
1840
|
+
->whereNull('deleted_at')
|
|
1841
|
+
->distanceSphere('location', $pickup, $distance)
|
|
1842
|
+
->distanceSphereValue('location', $pickup)
|
|
1843
|
+
->withoutGlobalScopes()
|
|
1844
|
+
->get();
|
|
1845
|
+
|
|
1846
|
+
return $drivers;
|
|
1847
|
+
}
|
|
1928
1848
|
}
|
|
@@ -368,6 +368,14 @@ class OrderConfig extends Model
|
|
|
368
368
|
return collect();
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Gets an activity from the order config by it's code.
|
|
373
|
+
*/
|
|
374
|
+
public function getActivityByCode(string $code): ?Activity
|
|
375
|
+
{
|
|
376
|
+
return $this->activities()->firstWhere('code', $code);
|
|
377
|
+
}
|
|
378
|
+
|
|
371
379
|
/**
|
|
372
380
|
* Creates an Activity instance representing a canceled order.
|
|
373
381
|
*
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Models;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Casts\Json;
|
|
6
|
+
use Fleetbase\FleetOps\Traits\Maintainable;
|
|
7
|
+
use Fleetbase\Models\Alert;
|
|
8
|
+
use Fleetbase\Models\File;
|
|
9
|
+
use Fleetbase\Models\Model;
|
|
10
|
+
use Fleetbase\Models\User;
|
|
11
|
+
use Fleetbase\Traits\HasApiModelBehavior;
|
|
12
|
+
use Fleetbase\Traits\HasMetaAttributes;
|
|
13
|
+
use Fleetbase\Traits\HasPublicId;
|
|
14
|
+
use Fleetbase\Traits\HasUuid;
|
|
15
|
+
use Fleetbase\Traits\Searchable;
|
|
16
|
+
use Fleetbase\Traits\TracksApiCredential;
|
|
17
|
+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
18
|
+
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
19
|
+
use Spatie\Activitylog\LogOptions;
|
|
20
|
+
use Spatie\Activitylog\Traits\LogsActivity;
|
|
21
|
+
use Spatie\Sluggable\HasSlug;
|
|
22
|
+
use Spatie\Sluggable\SlugOptions;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Class Part.
|
|
26
|
+
*
|
|
27
|
+
* Represents a stocked, replaceable component in the fleet management system.
|
|
28
|
+
* Parts can include filters, tires, belts, and other components with quantity and cost tracking.
|
|
29
|
+
*/
|
|
30
|
+
class Part extends Model
|
|
31
|
+
{
|
|
32
|
+
use HasUuid;
|
|
33
|
+
use HasPublicId;
|
|
34
|
+
use TracksApiCredential;
|
|
35
|
+
use Maintainable;
|
|
36
|
+
use HasApiModelBehavior;
|
|
37
|
+
use HasSlug;
|
|
38
|
+
use LogsActivity;
|
|
39
|
+
use HasMetaAttributes;
|
|
40
|
+
use Searchable;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The database table used by the model.
|
|
44
|
+
*
|
|
45
|
+
* @var string
|
|
46
|
+
*/
|
|
47
|
+
protected $table = 'parts';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The type of public Id to generate.
|
|
51
|
+
*
|
|
52
|
+
* @var string
|
|
53
|
+
*/
|
|
54
|
+
protected $publicIdType = 'part';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The attributes that can be queried.
|
|
58
|
+
*
|
|
59
|
+
* @var array
|
|
60
|
+
*/
|
|
61
|
+
protected $searchableColumns = ['sku', 'name', 'manufacturer', 'model', 'serial_number', 'barcode', 'public_id'];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The attributes that can be used for filtering.
|
|
65
|
+
*
|
|
66
|
+
* @var array
|
|
67
|
+
*/
|
|
68
|
+
protected $filterParams = ['vendor_uuid', 'manufacturer', 'status', 'asset_type', 'warranty_uuid'];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The attributes that are mass assignable.
|
|
72
|
+
*
|
|
73
|
+
* @var array
|
|
74
|
+
*/
|
|
75
|
+
protected $fillable = [
|
|
76
|
+
'company_uuid',
|
|
77
|
+
'vendor_uuid',
|
|
78
|
+
'warranty_uuid',
|
|
79
|
+
'photo_uuid',
|
|
80
|
+
'sku',
|
|
81
|
+
'name',
|
|
82
|
+
'manufacturer',
|
|
83
|
+
'model',
|
|
84
|
+
'serial_number',
|
|
85
|
+
'barcode',
|
|
86
|
+
'description',
|
|
87
|
+
'quantity_on_hand',
|
|
88
|
+
'unit_cost',
|
|
89
|
+
'msrp',
|
|
90
|
+
'asset_type',
|
|
91
|
+
'asset_uuid',
|
|
92
|
+
'type',
|
|
93
|
+
'status',
|
|
94
|
+
'specs',
|
|
95
|
+
'meta',
|
|
96
|
+
'slug',
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Dynamic attributes that are appended to object.
|
|
101
|
+
*
|
|
102
|
+
* @var array
|
|
103
|
+
*/
|
|
104
|
+
protected $appends = [
|
|
105
|
+
'vendor_name',
|
|
106
|
+
'warranty_name',
|
|
107
|
+
'photo_url',
|
|
108
|
+
'total_value',
|
|
109
|
+
'is_in_stock',
|
|
110
|
+
'is_low_stock',
|
|
111
|
+
'asset_name',
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The attributes excluded from the model's JSON form.
|
|
116
|
+
*
|
|
117
|
+
* @var array
|
|
118
|
+
*/
|
|
119
|
+
protected $hidden = ['vendor', 'warranty', 'photo', 'asset'];
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The attributes that should be cast to native types.
|
|
123
|
+
*
|
|
124
|
+
* @var array
|
|
125
|
+
*/
|
|
126
|
+
protected $casts = [
|
|
127
|
+
'quantity_on_hand' => 'integer',
|
|
128
|
+
'unit_cost' => 'decimal:2',
|
|
129
|
+
'msrp' => 'decimal:2',
|
|
130
|
+
'specs' => Json::class,
|
|
131
|
+
'meta' => Json::class,
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Properties which activity needs to be logged.
|
|
136
|
+
*
|
|
137
|
+
* @var array
|
|
138
|
+
*/
|
|
139
|
+
protected static $logAttributes = '*';
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Do not log empty changed.
|
|
143
|
+
*
|
|
144
|
+
* @var bool
|
|
145
|
+
*/
|
|
146
|
+
protected static $submitEmptyLogs = false;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The name of the subject to log.
|
|
150
|
+
*
|
|
151
|
+
* @var string
|
|
152
|
+
*/
|
|
153
|
+
protected static $logName = 'part';
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get the options for generating the slug.
|
|
157
|
+
*/
|
|
158
|
+
public function getSlugOptions(): SlugOptions
|
|
159
|
+
{
|
|
160
|
+
return SlugOptions::create()
|
|
161
|
+
->generateSlugsFrom(['name', 'sku'])
|
|
162
|
+
->saveSlugsTo('slug');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get the activity log options for the model.
|
|
167
|
+
*/
|
|
168
|
+
public function getActivitylogOptions(): LogOptions
|
|
169
|
+
{
|
|
170
|
+
return LogOptions::defaults()->logAll();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public function vendor(): BelongsTo
|
|
174
|
+
{
|
|
175
|
+
return $this->belongsTo(Vendor::class, 'vendor_uuid', 'uuid');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public function warranty(): BelongsTo
|
|
179
|
+
{
|
|
180
|
+
return $this->belongsTo(Warranty::class, 'warranty_uuid', 'uuid');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public function photo(): BelongsTo
|
|
184
|
+
{
|
|
185
|
+
return $this->belongsTo(File::class, 'photo_uuid', 'uuid');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public function createdBy(): BelongsTo
|
|
189
|
+
{
|
|
190
|
+
return $this->belongsTo(User::class, 'created_by_uuid', 'uuid');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public function updatedBy(): BelongsTo
|
|
194
|
+
{
|
|
195
|
+
return $this->belongsTo(User::class, 'updated_by_uuid', 'uuid');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public function asset(): MorphTo
|
|
199
|
+
{
|
|
200
|
+
return $this->morphTo();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get the vendor name.
|
|
205
|
+
*/
|
|
206
|
+
public function getVendorNameAttribute(): ?string
|
|
207
|
+
{
|
|
208
|
+
return $this->vendor?->name;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get the warranty name.
|
|
213
|
+
*/
|
|
214
|
+
public function getWarrantyNameAttribute(): ?string
|
|
215
|
+
{
|
|
216
|
+
return $this->warranty?->name;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get the photo URL.
|
|
221
|
+
*/
|
|
222
|
+
public function getPhotoUrlAttribute(): ?string
|
|
223
|
+
{
|
|
224
|
+
return $this->photo?->url;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get the total value of parts in stock.
|
|
229
|
+
*/
|
|
230
|
+
public function getTotalValueAttribute(): float
|
|
231
|
+
{
|
|
232
|
+
return $this->quantity_on_hand * ($this->unit_cost ?? 0);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check if the part is in stock.
|
|
237
|
+
*/
|
|
238
|
+
public function getIsInStockAttribute(): bool
|
|
239
|
+
{
|
|
240
|
+
return $this->quantity_on_hand > 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Check if the part is low on stock.
|
|
245
|
+
*/
|
|
246
|
+
public function getIsLowStockAttribute(): bool
|
|
247
|
+
{
|
|
248
|
+
$specs = $this->specs ?? [];
|
|
249
|
+
$lowStockThreshold = $specs['low_stock_threshold'] ?? 5;
|
|
250
|
+
|
|
251
|
+
return $this->quantity_on_hand <= $lowStockThreshold;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get the asset name if associated.
|
|
256
|
+
*/
|
|
257
|
+
public function getAssetNameAttribute(): ?string
|
|
258
|
+
{
|
|
259
|
+
if ($this->asset) {
|
|
260
|
+
return $this->asset->name ?? $this->asset->display_name ?? null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Scope to get parts in stock.
|
|
268
|
+
*
|
|
269
|
+
* @param \Illuminate\Database\Eloquent\Builder $query
|
|
270
|
+
*
|
|
271
|
+
* @return \Illuminate\Database\Eloquent\Builder
|
|
272
|
+
*/
|
|
273
|
+
public function scopeInStock($query)
|
|
274
|
+
{
|
|
275
|
+
return $query->where('quantity_on_hand', '>', 0);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Scope to get parts that are out of stock.
|
|
280
|
+
*
|
|
281
|
+
* @param \Illuminate\Database\Eloquent\Builder $query
|
|
282
|
+
*
|
|
283
|
+
* @return \Illuminate\Database\Eloquent\Builder
|
|
284
|
+
*/
|
|
285
|
+
public function scopeOutOfStock($query)
|
|
286
|
+
{
|
|
287
|
+
return $query->where('quantity_on_hand', '<=', 0);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Scope to get parts with low stock.
|
|
292
|
+
*
|
|
293
|
+
* @param \Illuminate\Database\Eloquent\Builder $query
|
|
294
|
+
*
|
|
295
|
+
* @return \Illuminate\Database\Eloquent\Builder
|
|
296
|
+
*/
|
|
297
|
+
public function scopeLowStock($query, ?int $threshold = null)
|
|
298
|
+
{
|
|
299
|
+
$threshold = $threshold ?? 5;
|
|
300
|
+
|
|
301
|
+
return $query->where('quantity_on_hand', '<=', $threshold)
|
|
302
|
+
->where('quantity_on_hand', '>', 0);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Scope to get parts by manufacturer.
|
|
307
|
+
*
|
|
308
|
+
* @param \Illuminate\Database\Eloquent\Builder $query
|
|
309
|
+
*
|
|
310
|
+
* @return \Illuminate\Database\Eloquent\Builder
|
|
311
|
+
*/
|
|
312
|
+
public function scopeByManufacturer($query, string $manufacturer)
|
|
313
|
+
{
|
|
314
|
+
return $query->where('manufacturer', $manufacturer);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Add stock to the part.
|
|
319
|
+
*/
|
|
320
|
+
public function addStock(int $quantity, ?string $reason = null): bool
|
|
321
|
+
{
|
|
322
|
+
if ($quantity <= 0) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
$oldQuantity = $this->quantity_on_hand;
|
|
327
|
+
$newQuantity = $oldQuantity + $quantity;
|
|
328
|
+
|
|
329
|
+
$updated = $this->update(['quantity_on_hand' => $newQuantity]);
|
|
330
|
+
|
|
331
|
+
if ($updated) {
|
|
332
|
+
activity('stock_added')
|
|
333
|
+
->performedOn($this)
|
|
334
|
+
->withProperties([
|
|
335
|
+
'old_quantity' => $oldQuantity,
|
|
336
|
+
'added_quantity' => $quantity,
|
|
337
|
+
'new_quantity' => $newQuantity,
|
|
338
|
+
'reason' => $reason,
|
|
339
|
+
])
|
|
340
|
+
->log("Added {$quantity} units to stock");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return $updated;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Remove stock from the part.
|
|
348
|
+
*/
|
|
349
|
+
public function removeStock(int $quantity, ?string $reason = null): bool
|
|
350
|
+
{
|
|
351
|
+
if ($quantity <= 0 || $quantity > $this->quantity_on_hand) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
$oldQuantity = $this->quantity_on_hand;
|
|
356
|
+
$newQuantity = $oldQuantity - $quantity;
|
|
357
|
+
|
|
358
|
+
$updated = $this->update(['quantity_on_hand' => $newQuantity]);
|
|
359
|
+
|
|
360
|
+
if ($updated) {
|
|
361
|
+
activity('stock_removed')
|
|
362
|
+
->performedOn($this)
|
|
363
|
+
->withProperties([
|
|
364
|
+
'old_quantity' => $oldQuantity,
|
|
365
|
+
'removed_quantity' => $quantity,
|
|
366
|
+
'new_quantity' => $newQuantity,
|
|
367
|
+
'reason' => $reason,
|
|
368
|
+
])
|
|
369
|
+
->log("Removed {$quantity} units from stock");
|
|
370
|
+
|
|
371
|
+
// Check if stock is now low and create alert if needed
|
|
372
|
+
if ($this->is_low_stock) {
|
|
373
|
+
$this->createLowStockAlert();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return $updated;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Set the stock quantity.
|
|
382
|
+
*/
|
|
383
|
+
public function setStock(int $quantity, ?string $reason = null): bool
|
|
384
|
+
{
|
|
385
|
+
if ($quantity < 0) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
$oldQuantity = $this->quantity_on_hand;
|
|
390
|
+
$updated = $this->update(['quantity_on_hand' => $quantity]);
|
|
391
|
+
|
|
392
|
+
if ($updated) {
|
|
393
|
+
activity('stock_adjusted')
|
|
394
|
+
->performedOn($this)
|
|
395
|
+
->withProperties([
|
|
396
|
+
'old_quantity' => $oldQuantity,
|
|
397
|
+
'new_quantity' => $quantity,
|
|
398
|
+
'adjustment' => $quantity - $oldQuantity,
|
|
399
|
+
'reason' => $reason,
|
|
400
|
+
])
|
|
401
|
+
->log("Stock adjusted from {$oldQuantity} to {$quantity}");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return $updated;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Create a low stock alert.
|
|
409
|
+
*/
|
|
410
|
+
protected function createLowStockAlert(): void
|
|
411
|
+
{
|
|
412
|
+
// Check if there's already an open low stock alert
|
|
413
|
+
$existingAlert = Alert::where('subject_type', static::class)
|
|
414
|
+
->where('subject_uuid', $this->uuid)
|
|
415
|
+
->where('type', 'low_stock')
|
|
416
|
+
->where('status', 'open')
|
|
417
|
+
->first();
|
|
418
|
+
|
|
419
|
+
if (!$existingAlert) {
|
|
420
|
+
Alert::create([
|
|
421
|
+
'company_uuid' => $this->company_uuid,
|
|
422
|
+
'type' => 'low_stock',
|
|
423
|
+
'severity' => 'medium',
|
|
424
|
+
'status' => 'open',
|
|
425
|
+
'subject_type' => static::class,
|
|
426
|
+
'subject_uuid' => $this->uuid,
|
|
427
|
+
'message' => "Part '{$this->name}' (SKU: {$this->sku}) is low on stock ({$this->quantity_on_hand} remaining)",
|
|
428
|
+
'context' => [
|
|
429
|
+
'part_name' => $this->name,
|
|
430
|
+
'sku' => $this->sku,
|
|
431
|
+
'current_quantity' => $this->quantity_on_hand,
|
|
432
|
+
'low_stock_threshold' => $this->specs['low_stock_threshold'] ?? 5,
|
|
433
|
+
],
|
|
434
|
+
'triggered_at' => now(),
|
|
435
|
+
]);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Check if the part is compatible with an asset.
|
|
441
|
+
*/
|
|
442
|
+
public function isCompatibleWith(Asset $asset): bool
|
|
443
|
+
{
|
|
444
|
+
$specs = $this->specs ?? [];
|
|
445
|
+
$compatibleAssets = $specs['compatible_assets'] ?? [];
|
|
446
|
+
|
|
447
|
+
if (empty($compatibleAssets)) {
|
|
448
|
+
return true; // No restrictions
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check by asset type
|
|
452
|
+
if (in_array($asset->type, $compatibleAssets)) {
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Check by make/model
|
|
457
|
+
$assetIdentifier = $asset->make . ' ' . $asset->model;
|
|
458
|
+
if (in_array($assetIdentifier, $compatibleAssets)) {
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Get the reorder point for this part.
|
|
467
|
+
*/
|
|
468
|
+
public function getReorderPoint(): int
|
|
469
|
+
{
|
|
470
|
+
$specs = $this->specs ?? [];
|
|
471
|
+
|
|
472
|
+
return $specs['reorder_point'] ?? $specs['low_stock_threshold'] ?? 5;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Get the reorder quantity for this part.
|
|
477
|
+
*/
|
|
478
|
+
public function getReorderQuantity(): int
|
|
479
|
+
{
|
|
480
|
+
$specs = $this->specs ?? [];
|
|
481
|
+
|
|
482
|
+
return $specs['reorder_quantity'] ?? 10;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Check if the part needs to be reordered.
|
|
487
|
+
*/
|
|
488
|
+
public function needsReorder(): bool
|
|
489
|
+
{
|
|
490
|
+
return $this->quantity_on_hand <= $this->getReorderPoint();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Get the estimated cost for a quantity of this part.
|
|
495
|
+
*/
|
|
496
|
+
public function getEstimatedCost(int $quantity = 1, bool $useRetailPrice = false): float
|
|
497
|
+
{
|
|
498
|
+
$price = $useRetailPrice ? ($this->msrp ?? $this->unit_cost) : $this->unit_cost;
|
|
499
|
+
|
|
500
|
+
return $quantity * ($price ?? 0);
|
|
501
|
+
}
|
|
502
|
+
}
|