@fleetbase/fleetops-engine 0.6.45 → 0.6.46
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/admin/map-settings.hbs +26 -0
- package/addon/components/admin/map-settings.js +46 -0
- package/addon/components/admin/routing-settings.hbs +11 -0
- package/addon/components/cell/order-route-type.hbs +46 -0
- package/addon/components/cell/order-route-type.js +90 -0
- package/addon/components/cell/place-address.hbs +1 -0
- package/addon/components/display-place.hbs +45 -56
- package/addon/components/display-place.js +21 -0
- package/addon/components/layout/fleet-ops-sidebar/driver-listing.js +5 -5
- package/addon/components/layout/fleet-ops-sidebar/fleet-listing.js +5 -5
- package/addon/components/layout/fleet-ops-sidebar.js +8 -0
- package/addon/components/maintenance-schedule/form.js +2 -1
- package/addon/components/map/drawer/driver-listing.js +5 -5
- package/addon/components/map/drawer/place-listing.js +5 -5
- package/addon/components/map/drawer/position-listing.js +102 -21
- package/addon/components/map/drawer/vehicle-listing.js +5 -5
- package/addon/components/map/google-live-map.hbs +9 -0
- package/addon/components/map/google-live-map.js +445 -0
- package/addon/components/map/leaflet-live-map.hbs +186 -142
- package/addon/components/map/leaflet-live-map.js +121 -41
- package/addon/components/map/toolbar/visibility-control-panel.js +2 -2
- package/addon/components/map/toolbar/zones-panel.hbs +9 -9
- package/addon/components/map/toolbar.hbs +3 -3
- package/addon/components/map/toolbar.js +9 -1
- package/addon/components/modals/map-layer-form.hbs +132 -63
- package/addon/components/modals/orchestrator-import.hbs +4 -11
- package/addon/components/modals/orchestrator-import.js +17 -1
- package/addon/components/modals/update-order-activity.hbs +1 -1
- package/addon/components/orchestrator/order-pool.js +3 -2
- package/addon/components/orchestrator-workbench.js +29 -41
- package/addon/components/order/details/detail.js +2 -2
- package/addon/components/order/details/payload.hbs +5 -5
- package/addon/components/order/details/payload.js +18 -1
- package/addon/components/order/details/route.hbs +2 -2
- package/addon/components/order/details/route.js +76 -3
- package/addon/components/order/form/payload.hbs +33 -0
- package/addon/components/order/form/payload.js +1 -0
- package/addon/components/order/form/route.hbs +8 -8
- package/addon/components/order/form/route.js +78 -18
- package/addon/components/order/form.js +2 -2
- package/addon/components/order/route-editor.hbs +209 -147
- package/addon/components/order/route-editor.js +143 -102
- package/addon/components/place/address.hbs +1 -0
- package/addon/components/place/address.js +12 -0
- package/addon/components/route-list.hbs +43 -55
- package/addon/components/route-list.js +47 -0
- package/addon/components/route-optimization-wizard-panel.js +3 -4
- package/addon/components/vehicle/card.hbs +4 -4
- package/addon/controllers/maintenance/schedules/index.js +2 -1
- package/addon/controllers/operations/orders/index/details.js +110 -37
- package/addon/controllers/operations/orders/index/new.js +1 -1
- package/addon/controllers/operations/orders/index.js +8 -1
- package/addon/controllers/settings/map.js +97 -0
- package/addon/controllers/settings/routing.js +47 -12
- package/addon/extension.js +11 -0
- package/addon/helpers/place-address-html.js +6 -0
- package/addon/instance-initializers/register-google-routes.js +17 -0
- package/addon/instance-initializers/register-osrm.js +5 -0
- package/addon/routes/application.js +6 -0
- package/addon/routes/operations/orchestrator.js +13 -0
- package/addon/routes/operations/orders/index/details.js +29 -22
- package/addon/routes/operations/orders/index/new.js +9 -0
- package/addon/routes/operations/orders/index.js +0 -1
- package/addon/routes/settings/map.js +3 -0
- package/addon/routes.js +1 -0
- package/addon/services/entity-actions.js +1 -1
- package/addon/services/geofence-event-bus.js +2 -1
- package/addon/services/geofence.js +204 -168
- package/addon/services/google-routes.js +300 -0
- package/addon/services/leaflet-draw-restriction.js +2 -1
- package/addon/services/leaflet-map-manager.js +17 -0
- package/addon/services/map-adapter/google.js +2205 -0
- package/addon/services/map-adapter/leaflet.js +1042 -0
- package/addon/services/map-adapter-interface.js +551 -0
- package/addon/services/map-manager.js +768 -0
- package/addon/services/map-settings.js +75 -0
- package/addon/services/movement-tracker.js +83 -32
- package/addon/services/order-actions.js +11 -1
- package/addon/services/osrm.js +79 -6
- package/addon/services/position-playback.js +135 -62
- package/addon/services/route-engine.js +83 -0
- package/addon/services/service-areas.js +8 -1
- package/addon/styles/fleetops-engine.css +555 -0
- package/addon/templates/management/vehicles/index.hbs +0 -1
- package/addon/templates/operations/orders/index/details.hbs +0 -1
- package/addon/templates/settings/map.hbs +53 -0
- package/addon/templates/settings/routing.hbs +15 -4
- package/addon/utils/fleet-ops-options.js +1 -0
- package/addon/utils/has-valid-coordinates.js +6 -0
- package/addon/utils/map-provider-options.js +6 -0
- package/addon/utils/order-route-editing.js +86 -0
- package/addon/utils/order-route-summary.js +20 -0
- package/addon/utils/place-address-html.js +41 -0
- package/addon/utils/route-colors.js +43 -12
- package/addon/utils/route-visualization.js +93 -0
- package/addon/utils/vendor-integration.js +2 -1
- package/app/components/admin/map-settings.js +1 -0
- package/app/components/admin/routing-settings.js +1 -0
- package/app/components/cell/order-route-type.js +1 -0
- package/app/components/cell/place-address.js +1 -0
- package/app/components/map/google-live-map.js +1 -0
- package/app/components/place/address.js +1 -0
- package/app/controllers/settings/map.js +1 -0
- package/app/helpers/place-address-html.js +1 -0
- package/app/routes/settings/map.js +1 -0
- package/app/services/map-adapter/google.js +1 -0
- package/app/services/map-adapter/leaflet.js +1 -0
- package/app/services/map-adapter-interface.js +1 -0
- package/app/services/map-manager.js +1 -0
- package/app/services/map-settings.js +1 -0
- package/app/templates/settings/map.js +1 -0
- package/app/utils/has-valid-coordinates.js +1 -0
- package/app/utils/order-route-editing.js +2 -0
- package/app/utils/order-route-summary.js +1 -0
- package/app/utils/place-address-html.js +1 -0
- package/app/utils/route-colors.js +1 -1
- package/app/utils/route-visualization.js +1 -0
- package/assets/images/drawing-controls-spritesheet.svg +156 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/src/Auth/Schemas/FleetOps.php +5 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +31 -3
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +14 -1
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -0
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +3 -2
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +2 -1
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +7 -4
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +61 -34
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +3 -2
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +106 -3
- package/server/src/Http/Filter/OrderFilter.php +2 -1
- package/server/src/Http/Resources/v1/Index/Payload.php +9 -7
- package/server/src/Http/Resources/v1/Order.php +20 -1
- package/server/src/Models/Order.php +42 -1
- package/server/src/Models/OrderConfig.php +15 -0
- package/server/src/Models/Payload.php +131 -27
- package/server/src/Models/Place.php +182 -48
- package/server/src/Models/ServiceRate.php +4 -3
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +59 -23
- package/server/src/Support/Utils.php +36 -2
- package/server/src/routes.php +4 -0
- package/translations/en-us.yaml +1937 -1916
|
@@ -1732,6 +1732,16 @@ class Order extends Model
|
|
|
1732
1732
|
|
|
1733
1733
|
if (Str::isUuid($this->order_config_uuid)) {
|
|
1734
1734
|
$orderConfig = OrderConfig::where('uuid', $this->order_config_uuid)->withTrashed()->first();
|
|
1735
|
+
if ($orderConfig instanceof OrderConfig) {
|
|
1736
|
+
$orderConfig->setOrderContext($this);
|
|
1737
|
+
|
|
1738
|
+
return $orderConfig;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
$company = $this->relationLoaded('company') ? $this->company : $this->company()->first();
|
|
1743
|
+
$orderConfig = OrderConfig::defaultOrCreate($company);
|
|
1744
|
+
if ($orderConfig instanceof OrderConfig) {
|
|
1735
1745
|
$orderConfig->setOrderContext($this);
|
|
1736
1746
|
|
|
1737
1747
|
return $orderConfig;
|
|
@@ -1740,6 +1750,37 @@ class Order extends Model
|
|
|
1740
1750
|
return null;
|
|
1741
1751
|
}
|
|
1742
1752
|
|
|
1753
|
+
/**
|
|
1754
|
+
* Ensure the order has a persisted order config and normalized type.
|
|
1755
|
+
*/
|
|
1756
|
+
public function ensureOrderConfig(): ?OrderConfig
|
|
1757
|
+
{
|
|
1758
|
+
$orderConfig = $this->config();
|
|
1759
|
+
|
|
1760
|
+
if (!$orderConfig) {
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
$updates = [];
|
|
1765
|
+
|
|
1766
|
+
if ($this->order_config_uuid !== $orderConfig->uuid) {
|
|
1767
|
+
$updates['order_config_uuid'] = $orderConfig->uuid;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
if (empty($this->type) || $this->type === 'default' || $this->type !== $orderConfig->key) {
|
|
1771
|
+
$updates['type'] = $orderConfig->key;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
if (!empty($updates)) {
|
|
1775
|
+
$this->fill($updates);
|
|
1776
|
+
$this->save();
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
$this->setRelation('orderConfig', $orderConfig);
|
|
1780
|
+
|
|
1781
|
+
return $orderConfig;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1743
1784
|
/**
|
|
1744
1785
|
* Retrieves the flow configuration from the order config.
|
|
1745
1786
|
*
|
|
@@ -1753,7 +1794,7 @@ class Order extends Model
|
|
|
1753
1794
|
public function getConfigFlow(): array
|
|
1754
1795
|
{
|
|
1755
1796
|
$orderConfig = $this->config();
|
|
1756
|
-
if (is_array($orderConfig->flow)) {
|
|
1797
|
+
if ($orderConfig && is_array($orderConfig->flow)) {
|
|
1757
1798
|
return $orderConfig->flow;
|
|
1758
1799
|
}
|
|
1759
1800
|
|
|
@@ -5,6 +5,7 @@ namespace Fleetbase\FleetOps\Models;
|
|
|
5
5
|
use Fleetbase\Casts\Json;
|
|
6
6
|
use Fleetbase\FleetOps\Casts\OrderConfigEntities;
|
|
7
7
|
use Fleetbase\FleetOps\Flow\Activity;
|
|
8
|
+
use Fleetbase\FleetOps\Support\FleetOps;
|
|
8
9
|
use Fleetbase\Models\Company;
|
|
9
10
|
use Fleetbase\Models\Model;
|
|
10
11
|
use Fleetbase\Support\Auth;
|
|
@@ -498,4 +499,18 @@ class OrderConfig extends Model
|
|
|
498
499
|
{
|
|
499
500
|
return static::where(['namespace' => 'system:order-config:transport', 'company_uuid' => $company ? $company->uuid : session('company')])->first();
|
|
500
501
|
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Get the default transport config, creating it for the company if needed.
|
|
505
|
+
*/
|
|
506
|
+
public static function defaultOrCreate(?Company $company = null): ?self
|
|
507
|
+
{
|
|
508
|
+
$company ??= Company::where('uuid', session('company'))->first();
|
|
509
|
+
|
|
510
|
+
if (!$company) {
|
|
511
|
+
return static::default($company);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return static::default($company) ?? FleetOps::createTransportConfig($company);
|
|
515
|
+
}
|
|
501
516
|
}
|
|
@@ -137,6 +137,26 @@ class Payload extends Model
|
|
|
137
137
|
return $this->hasMany(Waypoint::class)->with(['place']);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* The first waypoint marker for lightweight index fallback.
|
|
142
|
+
*/
|
|
143
|
+
public function firstWaypointMarker()
|
|
144
|
+
{
|
|
145
|
+
return $this->hasOne(Waypoint::class, 'payload_uuid', 'uuid')
|
|
146
|
+
->oldestOfMany('order')
|
|
147
|
+
->with(['place']);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The last waypoint marker for lightweight index fallback.
|
|
152
|
+
*/
|
|
153
|
+
public function lastWaypointMarker()
|
|
154
|
+
{
|
|
155
|
+
return $this->hasOne(Waypoint::class, 'payload_uuid', 'uuid')
|
|
156
|
+
->latestOfMany('order')
|
|
157
|
+
->with(['place']);
|
|
158
|
+
}
|
|
159
|
+
|
|
140
160
|
public function getTotalEntitiesAttribute()
|
|
141
161
|
{
|
|
142
162
|
return $this->entities()->count();
|
|
@@ -534,48 +554,104 @@ class Payload extends Model
|
|
|
534
554
|
return $this;
|
|
535
555
|
}
|
|
536
556
|
|
|
537
|
-
$
|
|
557
|
+
$this->loadMissing('waypointMarkers');
|
|
558
|
+
$keptWaypointIds = [];
|
|
559
|
+
$availableWaypointMarkers = $this->waypointMarkers()->get();
|
|
538
560
|
|
|
539
|
-
// collect all place ids to insert
|
|
540
561
|
foreach ($waypoints as $index => $attributes) {
|
|
562
|
+
$raw = $attributes;
|
|
563
|
+
$type = data_get($raw, 'type', 'dropoff');
|
|
564
|
+
$customerUuidIn = data_get($raw, 'customer_uuid');
|
|
565
|
+
$customerPubIdIn = data_get($raw, 'customer_id');
|
|
566
|
+
$customerType = data_get($raw, 'customer_type', 'fleetops:contact');
|
|
567
|
+
|
|
541
568
|
if (Utils::isset($attributes, 'place') && is_array(Utils::get($attributes, 'place'))) {
|
|
542
569
|
$attributes = Utils::get($attributes, 'place');
|
|
543
570
|
}
|
|
544
571
|
|
|
545
|
-
|
|
546
|
-
|
|
572
|
+
$placeUuid = null;
|
|
573
|
+
|
|
574
|
+
if (
|
|
575
|
+
is_array($attributes)
|
|
576
|
+
&& isset($attributes['place_uuid'])
|
|
577
|
+
&& Place::where('uuid', $attributes['place_uuid'])->exists()
|
|
578
|
+
) {
|
|
579
|
+
$placeUuid = $attributes['place_uuid'];
|
|
580
|
+
} elseif (
|
|
581
|
+
is_array($attributes)
|
|
582
|
+
&& isset($attributes['uuid'])
|
|
583
|
+
&& ($resolvedUuid = Place::where('uuid', $attributes['uuid'])->value('uuid'))
|
|
584
|
+
) {
|
|
585
|
+
$placeUuid = $resolvedUuid;
|
|
586
|
+
} elseif (
|
|
587
|
+
is_array($attributes)
|
|
588
|
+
&& isset($attributes['id'])
|
|
589
|
+
&& ($resolvedUuid = Place::where('public_id', $attributes['id'])->value('uuid'))
|
|
590
|
+
) {
|
|
591
|
+
$placeUuid = $resolvedUuid;
|
|
547
592
|
} else {
|
|
548
|
-
$
|
|
549
|
-
|
|
593
|
+
$place = Place::createFromMixed($attributes);
|
|
594
|
+
|
|
595
|
+
if ($place instanceof Place && isset($attributes['uuid']) && $place->uuid !== $attributes['uuid']) {
|
|
596
|
+
$place->updateMeta('search_uuid', $attributes['uuid']);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
$placeUuid = $place->uuid;
|
|
550
600
|
}
|
|
551
|
-
}
|
|
552
601
|
|
|
553
|
-
|
|
554
|
-
|
|
602
|
+
$customerUuid = null;
|
|
603
|
+
$customerTypeNamespace = null;
|
|
555
604
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
605
|
+
if ($customerType) {
|
|
606
|
+
$customerTypeNamespace = Utils::getMutationType($customerType);
|
|
607
|
+
$customerModel = app($customerTypeNamespace);
|
|
608
|
+
|
|
609
|
+
if ($customerUuidIn && $customerModel->where('uuid', $customerUuidIn)->exists()) {
|
|
610
|
+
$customerUuid = $customerUuidIn;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (!$customerUuid && $customerPubIdIn) {
|
|
614
|
+
$maybeUuid = $customerModel->where('public_id', $customerPubIdIn)->value('uuid');
|
|
615
|
+
if ($maybeUuid) {
|
|
616
|
+
$customerUuid = $maybeUuid;
|
|
617
|
+
} else {
|
|
618
|
+
$customerTypeNamespace = null;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (!$customerUuid) {
|
|
623
|
+
$customerTypeNamespace = null;
|
|
624
|
+
}
|
|
560
625
|
}
|
|
561
626
|
|
|
562
|
-
|
|
563
|
-
|
|
627
|
+
$values = [
|
|
628
|
+
'payload_uuid' => $this->uuid,
|
|
629
|
+
'place_uuid' => $placeUuid,
|
|
630
|
+
'order' => $index,
|
|
631
|
+
'type' => $type,
|
|
632
|
+
'customer_uuid' => $customerUuid,
|
|
633
|
+
'customer_type' => $customerTypeNamespace,
|
|
634
|
+
];
|
|
635
|
+
|
|
636
|
+
$existingWaypointMarker = $availableWaypointMarkers
|
|
637
|
+
->first(fn ($waypointMarker) => $waypointMarker->place_uuid === $placeUuid && !in_array($waypointMarker->uuid, $keptWaypointIds));
|
|
638
|
+
|
|
639
|
+
if ($existingWaypointMarker) {
|
|
640
|
+
$existingWaypointMarker->update($values);
|
|
641
|
+
$keptWaypointIds[] = $existingWaypointMarker->uuid;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
564
644
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
Waypoint::updateOrCreate(
|
|
568
|
-
[
|
|
569
|
-
'payload_uuid' => $this->uuid,
|
|
570
|
-
'place_uuid' => $placeId,
|
|
571
|
-
],
|
|
572
|
-
[
|
|
573
|
-
'payload_uuid' => $this->uuid,
|
|
574
|
-
'place_uuid' => $placeId,
|
|
575
|
-
]
|
|
576
|
-
);
|
|
645
|
+
$waypointRecord = Waypoint::create($values);
|
|
646
|
+
$keptWaypointIds[] = $waypointRecord->uuid;
|
|
577
647
|
}
|
|
578
648
|
|
|
649
|
+
$this->waypointMarkers()
|
|
650
|
+
->whereNotIn('uuid', $keptWaypointIds)
|
|
651
|
+
->get()
|
|
652
|
+
->each
|
|
653
|
+
->delete();
|
|
654
|
+
|
|
579
655
|
return $this->refresh()->load(['waypoints']);
|
|
580
656
|
}
|
|
581
657
|
|
|
@@ -673,6 +749,34 @@ class Payload extends Model
|
|
|
673
749
|
return null;
|
|
674
750
|
}
|
|
675
751
|
|
|
752
|
+
/**
|
|
753
|
+
* Get the lightweight pickup fallback for index resources.
|
|
754
|
+
*/
|
|
755
|
+
public function getIndexPickupPlaceAttribute(): ?Place
|
|
756
|
+
{
|
|
757
|
+
if ($this->pickup instanceof Place) {
|
|
758
|
+
return $this->pickup;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
$this->loadMissing('firstWaypointMarker.place');
|
|
762
|
+
|
|
763
|
+
return data_get($this, 'firstWaypointMarker.place');
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Get the lightweight dropoff fallback for index resources.
|
|
768
|
+
*/
|
|
769
|
+
public function getIndexDropoffPlaceAttribute(): ?Place
|
|
770
|
+
{
|
|
771
|
+
if ($this->dropoff instanceof Place) {
|
|
772
|
+
return $this->dropoff;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
$this->loadMissing('lastWaypointMarker.place');
|
|
776
|
+
|
|
777
|
+
return data_get($this, 'lastWaypointMarker.place');
|
|
778
|
+
}
|
|
779
|
+
|
|
676
780
|
public function getPickupRegion(): string
|
|
677
781
|
{
|
|
678
782
|
$pickup = $this->getPickupOrCurrentWaypoint();
|
|
@@ -335,24 +335,8 @@ class Place extends Model
|
|
|
335
335
|
|
|
336
336
|
// Before saving or returning this instance check the database for a duplicate address
|
|
337
337
|
// it cannot have any owner, and must belong to this session
|
|
338
|
-
if ($
|
|
339
|
-
$duplicate
|
|
340
|
-
'company_uuid' => $companyUuid,
|
|
341
|
-
'street1' => $instance->street1,
|
|
342
|
-
'city' => $instance->city,
|
|
343
|
-
'country' => $instance->country,
|
|
344
|
-
])
|
|
345
|
-
->when(
|
|
346
|
-
$instance->postal_code !== null,
|
|
347
|
-
fn ($q) => $q->where('postal_code', $instance->postal_code),
|
|
348
|
-
fn ($q) => $q->whereNull('postal_code')
|
|
349
|
-
)
|
|
350
|
-
->whereNull('owner_uuid')
|
|
351
|
-
->first();
|
|
352
|
-
|
|
353
|
-
if ($duplicate) {
|
|
354
|
-
return $duplicate;
|
|
355
|
-
}
|
|
338
|
+
if ($duplicate = static::findExistingSharedPlace($instance->toArray())) {
|
|
339
|
+
return $duplicate;
|
|
356
340
|
}
|
|
357
341
|
|
|
358
342
|
if ($saveInstance) {
|
|
@@ -400,7 +384,7 @@ class Place extends Model
|
|
|
400
384
|
/**
|
|
401
385
|
* Create a new Place instance from a geocoding lookup.
|
|
402
386
|
*
|
|
403
|
-
* @return
|
|
387
|
+
* @return array
|
|
404
388
|
*/
|
|
405
389
|
public static function getValuesFromGeocodingLookup(string $address): array
|
|
406
390
|
{
|
|
@@ -413,6 +397,163 @@ class Place extends Model
|
|
|
413
397
|
return static::getGoogleAddressArray($results->first());
|
|
414
398
|
}
|
|
415
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Compose a fuller geocoding query from structured place attributes.
|
|
402
|
+
*/
|
|
403
|
+
public static function composeGeocodingQuery(array $place): ?string
|
|
404
|
+
{
|
|
405
|
+
$parts = [];
|
|
406
|
+
|
|
407
|
+
foreach (['street1', 'street2', 'city', 'province', 'postal_code', 'country'] as $field) {
|
|
408
|
+
$value = static::normalizePlaceValue(data_get($place, $field));
|
|
409
|
+
if ($value) {
|
|
410
|
+
$parts[] = $value;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (empty($parts)) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return implode(', ', array_values(array_unique($parts)));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Normalize a place field value for matching and persistence.
|
|
423
|
+
*/
|
|
424
|
+
public static function normalizePlaceValue($value): ?string
|
|
425
|
+
{
|
|
426
|
+
if (!is_scalar($value)) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
$value = trim((string) $value);
|
|
431
|
+
|
|
432
|
+
return $value !== '' ? $value : null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Merge explicit structured place attributes with geocoded fallbacks.
|
|
437
|
+
* Explicit values always win.
|
|
438
|
+
*/
|
|
439
|
+
public static function mergeStructuredPlaceAttributes(array $place, array $geocoded = []): array
|
|
440
|
+
{
|
|
441
|
+
$place = array_merge($geocoded, $place);
|
|
442
|
+
|
|
443
|
+
foreach (['name', 'street1', 'street2', 'city', 'province', 'postal_code', 'country', 'phone'] as $field) {
|
|
444
|
+
$normalized = static::normalizePlaceValue(data_get($place, $field));
|
|
445
|
+
if ($normalized !== null) {
|
|
446
|
+
$place[$field] = $normalized;
|
|
447
|
+
} elseif (array_key_exists($field, $place)) {
|
|
448
|
+
$place[$field] = null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!empty($place['location'])) {
|
|
453
|
+
$place['location'] = Utils::getPointFromMixed($place['location']) ?? $place['location'];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return array_filter($place, fn ($value) => $value !== '');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Find an existing shared place using normalized address identity.
|
|
461
|
+
*/
|
|
462
|
+
public static function findExistingSharedPlace(array $place): ?Place
|
|
463
|
+
{
|
|
464
|
+
if (data_get($place, 'owner_uuid')) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
$companyUuid = data_get($place, 'company_uuid', session('company'));
|
|
469
|
+
$street1 = static::normalizePlaceValue(data_get($place, 'street1'));
|
|
470
|
+
$city = static::normalizePlaceValue(data_get($place, 'city'));
|
|
471
|
+
$country = static::normalizePlaceValue(data_get($place, 'country'));
|
|
472
|
+
|
|
473
|
+
if (!$companyUuid || !$street1 || !$city || !$country) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
$postalCode = static::normalizePlaceValue(data_get($place, 'postal_code'));
|
|
478
|
+
$street2 = static::normalizePlaceValue(data_get($place, 'street2'));
|
|
479
|
+
$location = Utils::getPointFromMixed(data_get($place, 'location'));
|
|
480
|
+
|
|
481
|
+
$existingPlace = static::query()
|
|
482
|
+
->where('company_uuid', $companyUuid)
|
|
483
|
+
->whereNull('owner_uuid')
|
|
484
|
+
->whereNull('deleted_at')
|
|
485
|
+
->where('street1', $street1)
|
|
486
|
+
->where('city', $city)
|
|
487
|
+
->where('country', $country)
|
|
488
|
+
->when(
|
|
489
|
+
$postalCode !== null,
|
|
490
|
+
fn ($query) => $query->where('postal_code', $postalCode)
|
|
491
|
+
)
|
|
492
|
+
->when(
|
|
493
|
+
$postalCode === null,
|
|
494
|
+
fn ($query) => $query->whereNull('postal_code')
|
|
495
|
+
)
|
|
496
|
+
->when(
|
|
497
|
+
$street2 !== null,
|
|
498
|
+
fn ($query) => $query->where('street2', $street2)
|
|
499
|
+
)
|
|
500
|
+
->first();
|
|
501
|
+
|
|
502
|
+
if ($existingPlace) {
|
|
503
|
+
return $existingPlace;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (!$location instanceof SpatialPoint) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return static::query()
|
|
511
|
+
->where('company_uuid', $companyUuid)
|
|
512
|
+
->whereNull('owner_uuid')
|
|
513
|
+
->whereNull('deleted_at')
|
|
514
|
+
->where('street1', $street1)
|
|
515
|
+
->where('city', $city)
|
|
516
|
+
->where('country', $country)
|
|
517
|
+
->whereRaw('ST_Equals(location, ST_GeomFromText(?))', [
|
|
518
|
+
sprintf('POINT(%s %s)', $location->getLng(), $location->getLat()),
|
|
519
|
+
])
|
|
520
|
+
->first();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Update safe missing fields on an existing shared place.
|
|
525
|
+
*/
|
|
526
|
+
public static function hydrateSharedPlace(Place $existingPlace, array $values): Place
|
|
527
|
+
{
|
|
528
|
+
$updates = [];
|
|
529
|
+
|
|
530
|
+
foreach (['name', 'street2', 'province', 'postal_code', 'neighborhood', 'district', 'building', 'phone'] as $field) {
|
|
531
|
+
$current = static::normalizePlaceValue($existingPlace->{$field});
|
|
532
|
+
$next = static::normalizePlaceValue(data_get($values, $field));
|
|
533
|
+
|
|
534
|
+
if ($current === null && $next !== null) {
|
|
535
|
+
$updates[$field] = $next;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
$existingLocation = Utils::getPointFromMixed($existingPlace->location);
|
|
540
|
+
$location = Utils::getPointFromMixed(data_get($values, 'location'));
|
|
541
|
+
$hasZeroLocation = $existingLocation instanceof SpatialPoint
|
|
542
|
+
&& $existingLocation->getLat() == 0.0
|
|
543
|
+
&& $existingLocation->getLng() == 0.0;
|
|
544
|
+
|
|
545
|
+
if (($existingLocation === null || $hasZeroLocation) && $location instanceof SpatialPoint) {
|
|
546
|
+
$updates['location'] = $location;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (!empty($updates)) {
|
|
550
|
+
$existingPlace->fill($updates);
|
|
551
|
+
$existingPlace->save();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return $existingPlace;
|
|
555
|
+
}
|
|
556
|
+
|
|
416
557
|
/**
|
|
417
558
|
* Create a new Place instance from a geocoding lookup.
|
|
418
559
|
*
|
|
@@ -571,31 +712,33 @@ class Place extends Model
|
|
|
571
712
|
}
|
|
572
713
|
|
|
573
714
|
// Get public_id if supplied if set
|
|
574
|
-
$id = data_get($place, 'id')
|
|
715
|
+
$id = data_get($place, 'id') ?: data_get($place, 'public_id');
|
|
575
716
|
|
|
576
717
|
// If $place has a valid uuid and a matching Place object exists, return the uuid
|
|
577
718
|
if (Utils::isPublicId($id) && $existingPlace = static::where('public_id', $id)->first()) {
|
|
578
719
|
return $existingPlace;
|
|
579
720
|
}
|
|
580
721
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
722
|
+
$query = static::composeGeocodingQuery($place)
|
|
723
|
+
?? static::normalizePlaceValue(data_get($place, 'address'))
|
|
724
|
+
?? static::normalizePlaceValue(data_get($place, 'street1'));
|
|
725
|
+
|
|
726
|
+
$geocoded = [];
|
|
727
|
+
if ($query) {
|
|
728
|
+
$geocoded = static::getValuesFromGeocodingLookup($query);
|
|
585
729
|
}
|
|
586
730
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if ($
|
|
590
|
-
return static::
|
|
731
|
+
$values = static::mergeStructuredPlaceAttributes($place, $geocoded);
|
|
732
|
+
|
|
733
|
+
if ($existingPlace = static::findExistingSharedPlace($values)) {
|
|
734
|
+
return static::hydrateSharedPlace($existingPlace, $values);
|
|
591
735
|
}
|
|
592
736
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
$place['location'] = new SpatialPoint(0, 0);
|
|
737
|
+
if (empty($values['location'])) {
|
|
738
|
+
$values['location'] = new SpatialPoint(0, 0);
|
|
596
739
|
}
|
|
597
740
|
|
|
598
|
-
return static::create($
|
|
741
|
+
return static::create($values);
|
|
599
742
|
}
|
|
600
743
|
// If $place is a GoogleAddress object
|
|
601
744
|
elseif ($place instanceof \Geocoder\Provider\GoogleMaps\Model\GoogleAddress) {
|
|
@@ -655,7 +798,11 @@ class Place extends Model
|
|
|
655
798
|
}
|
|
656
799
|
}
|
|
657
800
|
|
|
658
|
-
$values = $place;
|
|
801
|
+
$values = is_array($place) ? $place : (array) $place;
|
|
802
|
+
|
|
803
|
+
if ($existingPlace = static::findExistingSharedPlace($values)) {
|
|
804
|
+
return $existingPlace->uuid;
|
|
805
|
+
}
|
|
659
806
|
|
|
660
807
|
// create a new place
|
|
661
808
|
return static::insertGetUuid((array) $values);
|
|
@@ -703,21 +850,8 @@ class Place extends Model
|
|
|
703
850
|
$values['location'] = Utils::parsePointToWkt($values['location']);
|
|
704
851
|
}
|
|
705
852
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
->select(['uuid'])->where([
|
|
709
|
-
'company_uuid' => session('company'),
|
|
710
|
-
'name' => $values['name'] ?? null,
|
|
711
|
-
'street1' => $values['street1'] ?? null,
|
|
712
|
-
])
|
|
713
|
-
->whereNull('deleted_at')
|
|
714
|
-
->first();
|
|
715
|
-
|
|
716
|
-
if ($existing) {
|
|
717
|
-
unset($values['uuid'], $values['created_at'], $values['_key'], $values['company_uuid']);
|
|
718
|
-
static::where('uuid', $existing->uuid)->update($values);
|
|
719
|
-
|
|
720
|
-
return $existing->uuid;
|
|
853
|
+
if ($existing = static::findExistingSharedPlace($values)) {
|
|
854
|
+
return static::hydrateSharedPlace($existing, $values)->uuid;
|
|
721
855
|
}
|
|
722
856
|
|
|
723
857
|
if (isset($values['meta']) && (is_object($values['meta']) || is_array($values['meta']))) {
|
|
@@ -229,7 +229,7 @@ class ServiceRate extends Model
|
|
|
229
229
|
*/
|
|
230
230
|
public function isAlgorithm(): bool
|
|
231
231
|
{
|
|
232
|
-
return $this->rate_calculation_method === 'algo';
|
|
232
|
+
return $this->rate_calculation_method === 'algo' || $this->rate_calculation_method === 'algorithm';
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/**
|
|
@@ -677,7 +677,7 @@ class ServiceRate extends Model
|
|
|
677
677
|
*
|
|
678
678
|
* @return array an array containing the calculated quote and line items
|
|
679
679
|
*/
|
|
680
|
-
public function quoteFromPreliminaryData($entities = [], $waypoints = [], ?int $totalDistance = 0, ?int $totalTime = 0, ?bool $isCashOnDelivery = false)
|
|
680
|
+
public function quoteFromPreliminaryData($entities = [], $waypoints = [], ?int $totalDistance = 0, ?int $totalTime = 0, ?bool $isCashOnDelivery = false, ?int $endpointCount = null)
|
|
681
681
|
{
|
|
682
682
|
$lines = collect();
|
|
683
683
|
$subTotal = data_get($this, 'base_fee', 0);
|
|
@@ -739,6 +739,7 @@ class ServiceRate extends Model
|
|
|
739
739
|
}
|
|
740
740
|
|
|
741
741
|
if ($this->isAlgorithm()) {
|
|
742
|
+
$resolvedEndpointCount = $endpointCount ?? $this->inferEndpointCountFromStops($waypoints);
|
|
742
743
|
$rateFee = $this->normalizeCalculatedMoney(Algo::exec(
|
|
743
744
|
$this->algorithm,
|
|
744
745
|
$this->buildAlgorithmVariables(
|
|
@@ -746,7 +747,7 @@ class ServiceRate extends Model
|
|
|
746
747
|
$waypoints,
|
|
747
748
|
$totalDistance,
|
|
748
749
|
$totalTime,
|
|
749
|
-
$
|
|
750
|
+
$resolvedEndpointCount
|
|
750
751
|
),
|
|
751
752
|
true
|
|
752
753
|
));
|