@fleetbase/fleetops-engine 0.6.47 → 0.6.49
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/routing-settings.hbs +71 -0
- package/addon/components/admin/routing-settings.js +124 -0
- package/addon/components/customer/orders.js +1 -1
- package/addon/components/display-place.hbs +1 -1
- package/addon/components/driver/details.hbs +5 -1
- package/addon/components/driver/form.hbs +11 -1
- package/addon/components/fleet-listing-panel.hbs +2 -2
- package/addon/components/fleet-listing-panel.js +10 -0
- package/addon/components/map/google-live-map.js +12 -73
- package/addon/components/map/leaflet-live-map.hbs +149 -33
- package/addon/components/map/order-list-overlay/order.hbs +18 -3
- package/addon/components/map/toolbar.hbs +2 -2
- package/addon/components/map/toolbar.js +10 -2
- package/addon/components/orchestrator/phase-builder.hbs +49 -5
- package/addon/components/orchestrator/phase-builder.js +34 -1
- package/addon/components/orchestrator/resource-panel.hbs +19 -1
- package/addon/components/orchestrator/resource-panel.js +4 -0
- package/addon/components/orchestrator-workbench.js +4 -1
- package/addon/components/order/details/purchase-rate.hbs +19 -22
- package/addon/components/order/details/route.hbs +26 -1
- package/addon/components/order/details/route.js +50 -0
- package/addon/components/order/details/tracking.hbs +171 -27
- package/addon/components/order/details/tracking.js +457 -0
- package/addon/components/order/form.hbs +2 -2
- package/addon/components/order/kanban-card.hbs +18 -3
- package/addon/components/order-list-overlay/order.hbs +18 -3
- package/addon/components/order-progress-card.hbs +21 -6
- package/addon/components/order-tracking-lookup.hbs +7 -7
- package/addon/components/order-tracking-lookup.js +1 -1
- package/addon/components/route-list.hbs +78 -8
- package/addon/components/route-list.js +48 -6
- package/addon/components/tracking-stop-progress.hbs +45 -0
- package/addon/components/tracking-stop-progress.js +79 -0
- package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
- package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
- package/addon/components/widget/fuel-efficiency.hbs +26 -0
- package/addon/components/widget/fuel-efficiency.js +43 -0
- package/addon/components/widget/geofence-violations.hbs +44 -0
- package/addon/components/widget/geofence-violations.js +55 -0
- package/addon/components/widget/issues-insights.hbs +43 -0
- package/addon/components/widget/issues-insights.js +62 -0
- package/addon/components/widget/kpi-active-orders.hbs +10 -0
- package/addon/components/widget/kpi-active-orders.js +5 -0
- package/addon/components/widget/kpi-aov.hbs +10 -0
- package/addon/components/widget/kpi-aov.js +5 -0
- package/addon/components/widget/kpi-distance.hbs +10 -0
- package/addon/components/widget/kpi-distance.js +5 -0
- package/addon/components/widget/kpi-drivers-online.hbs +10 -0
- package/addon/components/widget/kpi-drivers-online.js +5 -0
- package/addon/components/widget/kpi-earnings.hbs +10 -0
- package/addon/components/widget/kpi-earnings.js +5 -0
- package/addon/components/widget/kpi-open-issues.hbs +11 -0
- package/addon/components/widget/kpi-open-issues.js +5 -0
- package/addon/components/widget/kpi-tile.hbs +59 -0
- package/addon/components/widget/kpi-tile.js +168 -0
- package/addon/components/widget/live-fleet.hbs +122 -0
- package/addon/components/widget/live-fleet.js +365 -0
- package/addon/components/widget/maintenance-overview.hbs +45 -0
- package/addon/components/widget/maintenance-overview.js +40 -0
- package/addon/components/widget/on-time-delivery.hbs +27 -0
- package/addon/components/widget/on-time-delivery.js +73 -0
- package/addon/components/widget/operations-pulse.hbs +51 -0
- package/addon/components/widget/operations-pulse.js +173 -0
- package/addon/components/widget/orders-by-status.hbs +26 -0
- package/addon/components/widget/orders-by-status.js +54 -0
- package/addon/components/widget/revenue-trend.hbs +34 -0
- package/addon/components/widget/revenue-trend.js +67 -0
- package/addon/components/widget/top-drivers.hbs +44 -0
- package/addon/components/widget/top-drivers.js +54 -0
- package/addon/controllers/settings/routing.js +124 -1
- package/addon/extension.js +172 -3
- package/addon/helpers/format-duration.js +38 -0
- package/addon/routes/application.js +6 -0
- package/addon/services/map-adapter/google.js +13 -2
- package/addon/services/order-list-overlay.js +2 -2
- package/addon/styles/fleetops-engine.css +806 -46
- package/addon/templates/maintenance/equipment/index.hbs +1 -1
- package/addon/templates/maintenance/parts/index.hbs +1 -1
- package/addon/templates/maintenance/schedules/index.hbs +1 -1
- package/addon/templates/management/vehicles/index.hbs +1 -1
- package/addon/templates/settings/avatars.hbs +1 -1
- package/addon/templates/settings/custom-fields.hbs +1 -1
- package/addon/templates/settings/map.hbs +1 -1
- package/addon/templates/settings/navigator-app.hbs +1 -1
- package/addon/templates/settings/notifications.hbs +1 -1
- package/addon/templates/settings/orchestrator.hbs +1 -1
- package/addon/templates/settings/payments/index.hbs +1 -1
- package/addon/templates/settings/payments/onboard.hbs +1 -1
- package/addon/templates/settings/routing.hbs +82 -4
- package/addon/templates/settings/scheduling.hbs +1 -1
- package/addon/utils/live-map-card-content.js +171 -0
- package/app/components/tracking-stop-progress.js +1 -0
- package/app/components/widget/fuel-efficiency.js +1 -0
- package/app/components/widget/geofence-violations.js +1 -0
- package/app/components/widget/issues-insights.js +1 -0
- package/app/components/widget/kpi-active-orders.js +1 -0
- package/app/components/widget/kpi-aov.js +1 -0
- package/app/components/widget/kpi-distance.js +1 -0
- package/app/components/widget/kpi-drivers-online.js +1 -0
- package/app/components/widget/kpi-earnings.js +1 -0
- package/app/components/widget/kpi-open-issues.js +1 -0
- package/app/components/widget/kpi-tile.js +1 -0
- package/app/components/widget/live-fleet.js +1 -0
- package/app/components/widget/maintenance-overview.js +1 -0
- package/app/components/widget/on-time-delivery.js +1 -0
- package/app/components/widget/operations-pulse.js +1 -0
- package/app/components/widget/orders-by-status.js +1 -0
- package/app/components/widget/revenue-trend.js +1 -0
- package/app/components/widget/top-drivers.js +1 -0
- package/app/helpers/format-duration.js +1 -0
- package/app/utils/live-map-card-content.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/pnpm-workspace.yaml +1 -0
- package/server/config/fleetops.php +32 -11
- package/server/config/telematics.php +111 -1
- package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
- package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
- package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
- package/server/seeders/Testing/ConnectivitySeeder.php +212 -0
- package/server/seeders/Testing/FleetSeeder.php +341 -0
- package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
- package/server/seeders/Testing/NetworkSeeder.php +250 -0
- package/server/seeders/Testing/OrdersSeeder.php +214 -0
- package/server/seeders/Testing/TestingSeeder.php +40 -0
- package/server/seeders/Testing/VroomOrchestrationSeeder.php +409 -0
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
- package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
- package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +52 -21
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +164 -12
- package/server/src/Http/Resources/v1/Driver.php +2 -0
- package/server/src/Http/Resources/v1/Fleet.php +4 -0
- package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
- package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
- package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
- package/server/src/Models/Contact.php +122 -13
- package/server/src/Models/Driver.php +2 -0
- package/server/src/Models/Order.php +1 -1
- package/server/src/Models/OrderConfig.php +14 -11
- package/server/src/Models/Payload.php +8 -8
- package/server/src/Models/Place.php +0 -3
- package/server/src/Models/ServiceRate.php +1 -1
- package/server/src/Notifications/LateDeparture.php +48 -0
- package/server/src/Notifications/ProlongedStoppage.php +48 -0
- package/server/src/Notifications/RouteDeviation.php +48 -0
- package/server/src/Observers/ContactObserver.php +8 -0
- package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +357 -38
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
- package/server/src/Providers/FleetOpsServiceProvider.php +32 -0
- package/server/src/Providers/NotificationServiceProvider.php +3 -0
- package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
- package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
- package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
- package/server/src/Support/Analytics/IssuesInsights.php +67 -0
- package/server/src/Support/Analytics/LiveFleet.php +127 -0
- package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
- package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
- package/server/src/Support/Analytics/OperationsPulse.php +86 -0
- package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
- package/server/src/Support/Analytics/RevenueTrend.php +79 -0
- package/server/src/Support/Analytics/TopDrivers.php +92 -0
- package/server/src/Support/LiveOrderQuery.php +75 -0
- package/server/src/Support/Metrics/AbstractMetric.php +170 -0
- package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
- package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
- package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
- package/server/src/Support/Metrics/EarningsMetric.php +30 -0
- package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
- package/server/src/Support/Metrics/MoneyMetric.php +30 -0
- package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
- package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
- package/server/src/Support/Metrics/Registry.php +46 -0
- package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
- package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
- package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
- package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
- package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
- package/server/src/Support/Metrics.php +46 -234
- package/server/src/Support/OSRM.php +11 -6
- package/server/src/Support/OrderTracker.php +7 -582
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
- package/server/src/Support/Utils.php +1 -1
- package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
- package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
- package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
- package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
- package/server/src/Tracking/README.md +25 -0
- package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
- package/server/src/Tracking/TrackingContext.php +63 -0
- package/server/src/Tracking/TrackingContextBuilder.php +204 -0
- package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
- package/server/src/Tracking/TrackingOptions.php +49 -0
- package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
- package/server/src/Tracking/TrackingProviderManager.php +89 -0
- package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
- package/server/src/Tracking/TrackingProviderResult.php +20 -0
- package/server/src/Tracking/TrackingStop.php +59 -0
- package/server/src/routes.php +27 -0
- package/server/tests/AnalyticsRoutesTest.php +41 -0
- package/server/tests/CapacityAllocationEngineTest.php +108 -0
- package/server/tests/LiveOrderQueryTest.php +34 -0
- package/server/tests/MetricsRegistryTest.php +88 -0
- package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
- package/server/tests/OrchestratorConsumableApiTest.php +74 -0
- package/server/tests/PingDriverEndpointTest.php +19 -0
- package/server/tests/TrackingIntelligenceTest.php +176 -0
- package/server/tests/VroomOrchestrationEngineTest.php +250 -0
- package/translations/en-us.yaml +41 -0
|
@@ -141,85 +141,243 @@ class OrchestrationPayloadBuilder
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
|
-
* Build
|
|
144
|
+
* Build normalized route tasks from Fleetbase orders.
|
|
145
145
|
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* - pickup: [longitude, latitude] of the pickup (for PDPTW shipments)
|
|
150
|
-
* - service: service time in seconds (waypoint.service_time → order meta → default 300)
|
|
151
|
-
* - time_windows: [[earliest_unix, latest_unix]] from order.time_window_start/end or scheduled_at
|
|
152
|
-
* - skills: integer skill codes from order.required_skills or custom fields
|
|
153
|
-
* - amount: multi-dimensional capacity demand [weight_kg, volume_litres, pallets, parcels]
|
|
154
|
-
* - priority: orchestrator_priority (0–100, higher = more important)
|
|
155
|
-
* - description: human-readable label for debugging
|
|
146
|
+
* Fleetbase orders must remain atomic during allocation. A task contains
|
|
147
|
+
* the full ordered stop list for the order, but engine adapters decide how
|
|
148
|
+
* much of that route shape their solver can safely model.
|
|
156
149
|
*/
|
|
157
|
-
public static function
|
|
150
|
+
public static function buildRouteTasks(Collection $orders): array
|
|
158
151
|
{
|
|
159
152
|
return $orders->map(function (Order $order) {
|
|
160
|
-
$
|
|
161
|
-
$
|
|
153
|
+
$candidates = static::buildRouteStopCandidates($order);
|
|
154
|
+
$invalidStop = collect($candidates)->first(fn (array $stop) => !static::coordinatesFromPlace($stop['place'] ?? null));
|
|
155
|
+
$stops = static::normalizeRouteStops($candidates);
|
|
156
|
+
|
|
157
|
+
if ($invalidStop) {
|
|
158
|
+
return [
|
|
159
|
+
'id' => $order->public_id,
|
|
160
|
+
'description' => $order->public_id,
|
|
161
|
+
'invalid' => true,
|
|
162
|
+
'reason' => sprintf('Order %s stop is missing valid coordinates.', $invalidStop['role'] ?? 'route'),
|
|
163
|
+
'stops' => [],
|
|
164
|
+
];
|
|
165
|
+
}
|
|
162
166
|
|
|
163
|
-
if (
|
|
164
|
-
return
|
|
167
|
+
if (empty($stops)) {
|
|
168
|
+
return [
|
|
169
|
+
'id' => $order->public_id,
|
|
170
|
+
'description' => $order->public_id,
|
|
171
|
+
'invalid' => true,
|
|
172
|
+
'reason' => 'Order has no routable pickup, dropoff, or waypoint coordinates.',
|
|
173
|
+
'stops' => [],
|
|
174
|
+
];
|
|
165
175
|
}
|
|
166
176
|
|
|
167
|
-
|
|
168
|
-
$job = [
|
|
177
|
+
$task = [
|
|
169
178
|
'id' => $order->public_id,
|
|
170
|
-
'location' => [$destination->location->getLng(), $destination->location->getLat()],
|
|
171
179
|
'description' => $order->public_id,
|
|
180
|
+
'stops' => $stops,
|
|
181
|
+
'amount' => static::computePayloadDemand($order),
|
|
182
|
+
'service' => static::resolveServiceTime($order),
|
|
172
183
|
];
|
|
173
184
|
|
|
174
|
-
// Pickup location (for pickup-and-delivery problems)
|
|
175
|
-
$pickup = $payload?->pickup;
|
|
176
|
-
if ($pickup && $pickup->location) {
|
|
177
|
-
$job['pickup'] = [$pickup->location->getLng(), $pickup->location->getLat()];
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// --- Service time ---
|
|
181
|
-
// Prefer waypoint-level service_time, then order meta, then default 300s
|
|
182
|
-
$waypointMarker = $payload?->waypointMarkers?->last();
|
|
183
|
-
$job['service'] = (int) (
|
|
184
|
-
$waypointMarker?->service_time
|
|
185
|
-
?? $order->getMeta('service_time_seconds')
|
|
186
|
-
?? 300
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
// --- Capacity demand ---
|
|
190
|
-
// Aggregated dynamically from payload entities; falls back to order meta.
|
|
191
|
-
$job['amount'] = static::computePayloadDemand($order);
|
|
192
|
-
|
|
193
|
-
// --- Time windows ---
|
|
194
|
-
// Prefer explicit orchestrator time_window columns, fall back to scheduled_at
|
|
195
185
|
if ($order->time_window_start && $order->time_window_end) {
|
|
196
|
-
$
|
|
186
|
+
$task['time_windows'] = [[
|
|
197
187
|
$order->time_window_start->timestamp,
|
|
198
188
|
$order->time_window_end->timestamp,
|
|
199
189
|
]];
|
|
200
190
|
} elseif ($order->scheduled_at) {
|
|
201
|
-
$start
|
|
202
|
-
$end
|
|
203
|
-
$
|
|
191
|
+
$start = $order->scheduled_at->timestamp;
|
|
192
|
+
$end = $order->scheduled_at->copy()->addHours(4)->timestamp;
|
|
193
|
+
$task['time_windows'] = [[$start, $end]];
|
|
204
194
|
}
|
|
205
195
|
|
|
206
|
-
// --- Skills ---
|
|
207
|
-
// Prefer first-class required_skills JSON column, fall back to custom fields
|
|
208
196
|
$skills = static::resolveSkills(
|
|
209
197
|
$order->required_skills ?? [],
|
|
210
198
|
$order->custom_fields ?? []
|
|
211
199
|
);
|
|
212
200
|
if (!empty($skills)) {
|
|
213
|
-
$
|
|
201
|
+
$task['skills'] = $skills;
|
|
214
202
|
}
|
|
215
203
|
|
|
216
|
-
// --- Priority ---
|
|
217
204
|
if ($order->orchestrator_priority !== null && $order->orchestrator_priority > 0) {
|
|
218
|
-
$
|
|
205
|
+
$task['priority'] = (int) $order->orchestrator_priority;
|
|
219
206
|
}
|
|
220
207
|
|
|
221
|
-
return $
|
|
222
|
-
})->
|
|
208
|
+
return $task;
|
|
209
|
+
})->values()->toArray();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @deprecated Use buildRouteTasks(). Kept for older engine adapters.
|
|
214
|
+
*/
|
|
215
|
+
public static function buildJobs(Collection $orders): array
|
|
216
|
+
{
|
|
217
|
+
return array_values(array_filter(
|
|
218
|
+
static::buildRouteTasks($orders),
|
|
219
|
+
fn (array $task) => empty($task['invalid'])
|
|
220
|
+
));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Build normalized capacity-only tasks from Fleetbase orders.
|
|
225
|
+
*
|
|
226
|
+
* Capacity allocation does not require route stops. It only needs an order
|
|
227
|
+
* demand vector plus optional skills, priority, and task limits.
|
|
228
|
+
*/
|
|
229
|
+
public static function buildCapacityTasks(Collection $orders): array
|
|
230
|
+
{
|
|
231
|
+
return $orders->map(function (Order $order) {
|
|
232
|
+
if (!$order->payload) {
|
|
233
|
+
return [
|
|
234
|
+
'id' => $order->public_id,
|
|
235
|
+
'description' => $order->public_id,
|
|
236
|
+
'invalid' => true,
|
|
237
|
+
'reason' => 'Order has no payload to compute capacity demand.',
|
|
238
|
+
'amount' => [0, 0, 0, 0],
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
$task = [
|
|
243
|
+
'id' => $order->public_id,
|
|
244
|
+
'description' => $order->public_id,
|
|
245
|
+
'amount' => static::computePayloadDemand($order),
|
|
246
|
+
'service' => static::resolveServiceTime($order),
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
$skills = static::resolveSkills(
|
|
250
|
+
$order->required_skills ?? [],
|
|
251
|
+
$order->custom_fields ?? []
|
|
252
|
+
);
|
|
253
|
+
if (!empty($skills)) {
|
|
254
|
+
$task['skills'] = $skills;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if ($order->orchestrator_priority !== null && $order->orchestrator_priority > 0) {
|
|
258
|
+
$task['priority'] = (int) $order->orchestrator_priority;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return $task;
|
|
262
|
+
})->values()->toArray();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Return Fleetbase's canonical ordered stops for an order:
|
|
267
|
+
* pickup, then waypoints by marker order, then dropoff.
|
|
268
|
+
*/
|
|
269
|
+
public static function buildRouteStops(Order $order): array
|
|
270
|
+
{
|
|
271
|
+
return static::normalizeRouteStops(static::buildRouteStopCandidates($order));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
protected static function buildRouteStopCandidates(Order $order): array
|
|
275
|
+
{
|
|
276
|
+
$payload = $order->payload;
|
|
277
|
+
if (!$payload) {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
$stops = [];
|
|
282
|
+
|
|
283
|
+
if ($payload->pickup) {
|
|
284
|
+
$stops[] = ['role' => 'pickup', 'place' => $payload->pickup];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
$waypointMarkers = method_exists($payload, 'relationLoaded') && $payload->relationLoaded('waypointMarkers')
|
|
288
|
+
? $payload->waypointMarkers
|
|
289
|
+
: collect();
|
|
290
|
+
if ($waypointMarkers->isNotEmpty()) {
|
|
291
|
+
foreach ($waypointMarkers->sortBy('order')->values() as $index => $waypoint) {
|
|
292
|
+
$stops[] = [
|
|
293
|
+
'role' => 'waypoint',
|
|
294
|
+
'place' => $waypoint->place,
|
|
295
|
+
'waypoint_id' => $waypoint->public_id,
|
|
296
|
+
'waypoint_uuid' => $waypoint->uuid,
|
|
297
|
+
'order' => $waypoint->order ?? $index,
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
} elseif (isset($payload->waypoints) && $payload->waypoints) {
|
|
301
|
+
foreach ($payload->waypoints->values() as $index => $waypoint) {
|
|
302
|
+
$stops[] = [
|
|
303
|
+
'role' => 'waypoint',
|
|
304
|
+
'place' => $waypoint,
|
|
305
|
+
'order' => $index,
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if ($payload->dropoff) {
|
|
311
|
+
$stops[] = ['role' => 'dropoff', 'place' => $payload->dropoff];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return $stops;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
protected static function normalizeRouteStops(array $stops): array
|
|
318
|
+
{
|
|
319
|
+
return collect($stops)
|
|
320
|
+
->map(function (array $stop) {
|
|
321
|
+
$coordinates = static::coordinatesFromPlace($stop['place'] ?? null);
|
|
322
|
+
if (!$coordinates) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
unset($stop['place'], $stop['waypoint_uuid']);
|
|
327
|
+
|
|
328
|
+
return array_merge($stop, [
|
|
329
|
+
'location' => $coordinates,
|
|
330
|
+
]);
|
|
331
|
+
})
|
|
332
|
+
->filter()
|
|
333
|
+
->values()
|
|
334
|
+
->toArray();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
protected static function resolveServiceTime(Order $order): int
|
|
338
|
+
{
|
|
339
|
+
$payload = $order->payload;
|
|
340
|
+
$waypointMarker = $payload?->waypointMarkers?->last();
|
|
341
|
+
|
|
342
|
+
return (int) (
|
|
343
|
+
$waypointMarker?->service_time
|
|
344
|
+
?? $order->getMeta('service_time_seconds')
|
|
345
|
+
?? 300
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
protected static function coordinatesFromPlace($place): ?array
|
|
350
|
+
{
|
|
351
|
+
$location = $place?->location;
|
|
352
|
+
if (!$location) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
$lng = $location->getLng();
|
|
357
|
+
$lat = $location->getLat();
|
|
358
|
+
|
|
359
|
+
if (!static::isValidCoordinate($lng, $lat)) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return [(float) $lng, (float) $lat];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
protected static function isValidCoordinate($lng, $lat): bool
|
|
367
|
+
{
|
|
368
|
+
if (!is_numeric($lng) || !is_numeric($lat)) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
$lng = (float) $lng;
|
|
373
|
+
$lat = (float) $lat;
|
|
374
|
+
|
|
375
|
+
return is_finite($lng)
|
|
376
|
+
&& is_finite($lat)
|
|
377
|
+
&& $lng >= -180
|
|
378
|
+
&& $lng <= 180
|
|
379
|
+
&& $lat >= -90
|
|
380
|
+
&& $lat <= 90;
|
|
223
381
|
}
|
|
224
382
|
|
|
225
383
|
/**
|
|
@@ -337,6 +495,56 @@ class OrchestrationPayloadBuilder
|
|
|
337
495
|
})->filter()->values()->toArray();
|
|
338
496
|
}
|
|
339
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Build vehicles for non-geospatial capacity allocation.
|
|
500
|
+
*
|
|
501
|
+
* Unlike buildVehicles(), this intentionally does not require a vehicle or
|
|
502
|
+
* driver location because no routing engine will be asked to calculate
|
|
503
|
+
* travel distance or duration.
|
|
504
|
+
*/
|
|
505
|
+
public static function buildCapacityVehicles(Collection $vehicles): array
|
|
506
|
+
{
|
|
507
|
+
return $vehicles->map(function (Vehicle $vehicle) {
|
|
508
|
+
$driver = $vehicle->driver;
|
|
509
|
+
$entry = [
|
|
510
|
+
'id' => $vehicle->public_id,
|
|
511
|
+
'driver_id' => $driver?->public_id,
|
|
512
|
+
'capacity' => static::buildVehicleCapacity($vehicle),
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
if ($vehicle->max_tasks !== null && $vehicle->max_tasks > 0) {
|
|
516
|
+
$entry['max_tasks'] = (int) $vehicle->max_tasks;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
$maxTravel = $driver?->max_travel_time ?? static::safeMeta($vehicle, 'max_travel_time_seconds');
|
|
520
|
+
if ($maxTravel) {
|
|
521
|
+
$entry['max_travel_time'] = (int) $maxTravel;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if ($driver?->time_window_start && $driver?->time_window_end) {
|
|
525
|
+
$entry['time_window'] = [
|
|
526
|
+
$driver->time_window_start->timestamp,
|
|
527
|
+
$driver->time_window_end->timestamp,
|
|
528
|
+
];
|
|
529
|
+
} elseif ($vehicle->time_window_start && $vehicle->time_window_end) {
|
|
530
|
+
$entry['time_window'] = [
|
|
531
|
+
$vehicle->time_window_start->timestamp,
|
|
532
|
+
$vehicle->time_window_end->timestamp,
|
|
533
|
+
];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
$skills = array_values(array_unique(array_merge(
|
|
537
|
+
static::resolveSkills($vehicle->skills ?? [], $vehicle->custom_fields ?? []),
|
|
538
|
+
static::resolveSkills($driver?->skills ?? [], $driver?->custom_fields ?? [])
|
|
539
|
+
)));
|
|
540
|
+
if (!empty($skills)) {
|
|
541
|
+
$entry['skills'] = $skills;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return $entry;
|
|
545
|
+
})->values()->toArray();
|
|
546
|
+
}
|
|
547
|
+
|
|
340
548
|
/**
|
|
341
549
|
* Resolve integer skill codes from a first-class skills array and/or custom fields.
|
|
342
550
|
*
|
|
@@ -65,6 +65,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
65
65
|
\Fleetbase\FleetOps\Console\Commands\TestEmail::class,
|
|
66
66
|
\Fleetbase\FleetOps\Console\Commands\ProcessMaintenanceTriggers::class,
|
|
67
67
|
\Fleetbase\FleetOps\Console\Commands\SendMaintenanceReminders::class,
|
|
68
|
+
\Fleetbase\FleetOps\Console\Commands\ProcessOperationalAlerts::class,
|
|
68
69
|
];
|
|
69
70
|
|
|
70
71
|
/**
|
|
@@ -99,6 +100,13 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
99
100
|
\Fleetbase\FleetOps\Orchestration\OrchestrationEngineRegistry::class,
|
|
100
101
|
fn () => new \Fleetbase\FleetOps\Orchestration\OrchestrationEngineRegistry()
|
|
101
102
|
);
|
|
103
|
+
|
|
104
|
+
// Register the TrackingProviderRegistry as a singleton so FleetOps core
|
|
105
|
+
// and third-party extensions can share tracking intelligence providers.
|
|
106
|
+
$this->app->singleton(
|
|
107
|
+
\Fleetbase\FleetOps\Tracking\TrackingProviderRegistry::class,
|
|
108
|
+
fn () => new \Fleetbase\FleetOps\Tracking\TrackingProviderRegistry()
|
|
109
|
+
);
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
/**
|
|
@@ -119,6 +127,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
119
127
|
$schedule->command('fleetops:purge-service-quotes')->daily()->withoutOverlapping();
|
|
120
128
|
$schedule->command('fleetops:process-maintenance-triggers')->daily()->withoutOverlapping()->storeOutputInDb();
|
|
121
129
|
$schedule->command('fleetops:send-maintenance-reminders')->daily()->withoutOverlapping()->storeOutputInDb();
|
|
130
|
+
$schedule->command('fleetops:process-operational-alerts')->everyMinute()->withoutOverlapping()->storeOutputInDb();
|
|
122
131
|
});
|
|
123
132
|
$this->registerNotifications();
|
|
124
133
|
$this->registerExpansionsFrom(__DIR__ . '/../Expansions');
|
|
@@ -135,6 +144,26 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
135
144
|
if (!$registry->has('greedy')) {
|
|
136
145
|
$registry->register(new \Fleetbase\FleetOps\Orchestration\Engines\GreedyOrchestrationEngine());
|
|
137
146
|
}
|
|
147
|
+
if (!$registry->has('capacity')) {
|
|
148
|
+
$registry->register(new \Fleetbase\FleetOps\Orchestration\Engines\CapacityAllocationEngine());
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Register built-in tracking providers. Third-party extensions can
|
|
154
|
+
// register additional providers from their own service providers.
|
|
155
|
+
$this->app->resolving(
|
|
156
|
+
\Fleetbase\FleetOps\Tracking\TrackingProviderRegistry::class,
|
|
157
|
+
function (\Fleetbase\FleetOps\Tracking\TrackingProviderRegistry $registry) {
|
|
158
|
+
if (!$registry->has('google_routes')) {
|
|
159
|
+
$registry->register(new \Fleetbase\FleetOps\Tracking\Providers\GoogleRoutesTrackingProvider());
|
|
160
|
+
}
|
|
161
|
+
if (!$registry->has('osrm')) {
|
|
162
|
+
$registry->register(new \Fleetbase\FleetOps\Tracking\Providers\OsrmTrackingProvider());
|
|
163
|
+
}
|
|
164
|
+
if (!$registry->has('calculated')) {
|
|
165
|
+
$registry->register(new \Fleetbase\FleetOps\Tracking\Providers\CalculatedTrackingProvider());
|
|
166
|
+
}
|
|
138
167
|
}
|
|
139
168
|
);
|
|
140
169
|
$this->loadRoutesFrom(__DIR__ . '/../routes.php');
|
|
@@ -165,6 +194,9 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
165
194
|
\Fleetbase\FleetOps\Notifications\OrderFailed::class,
|
|
166
195
|
\Fleetbase\FleetOps\Notifications\OrderCompleted::class,
|
|
167
196
|
\Fleetbase\FleetOps\Notifications\DriverArrivedAtGeofence::class,
|
|
197
|
+
\Fleetbase\FleetOps\Notifications\LateDeparture::class,
|
|
198
|
+
\Fleetbase\FleetOps\Notifications\RouteDeviation::class,
|
|
199
|
+
\Fleetbase\FleetOps\Notifications\ProlongedStoppage::class,
|
|
168
200
|
]);
|
|
169
201
|
|
|
170
202
|
// Register Notifiables
|
|
@@ -31,6 +31,9 @@ class NotificationServiceProvider extends CoreServiceProvider
|
|
|
31
31
|
\Fleetbase\FleetOps\Notifications\OrderDispatched::class,
|
|
32
32
|
\Fleetbase\FleetOps\Notifications\OrderDispatchFailed::class,
|
|
33
33
|
\Fleetbase\FleetOps\Notifications\OrderPing::class,
|
|
34
|
+
\Fleetbase\FleetOps\Notifications\LateDeparture::class,
|
|
35
|
+
\Fleetbase\FleetOps\Notifications\RouteDeviation::class,
|
|
36
|
+
\Fleetbase\FleetOps\Notifications\ProlongedStoppage::class,
|
|
34
37
|
]);
|
|
35
38
|
|
|
36
39
|
// Register Notifiables
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Analytics;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Models\Company;
|
|
6
|
+
use Illuminate\Support\Carbon;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base for analytics-widget data providers. Unlike scalar Metrics, each
|
|
10
|
+
* subclass returns a widget-specific JSON shape (Chart.js datasets, ranked
|
|
11
|
+
* rows, scalar tiles, map points, etc.) so the base just provides company
|
|
12
|
+
* scoping + period parsing.
|
|
13
|
+
*/
|
|
14
|
+
abstract class AbstractAnalytics
|
|
15
|
+
{
|
|
16
|
+
protected Company $company;
|
|
17
|
+
protected ?\DateTimeInterface $start = null;
|
|
18
|
+
protected ?\DateTimeInterface $end = null;
|
|
19
|
+
|
|
20
|
+
abstract public function get(): array;
|
|
21
|
+
|
|
22
|
+
public static function forCompany(Company $company): static
|
|
23
|
+
{
|
|
24
|
+
return (new static())->setCompany($company);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public function between(?\DateTimeInterface $start, ?\DateTimeInterface $end): static
|
|
28
|
+
{
|
|
29
|
+
$this->start = $start;
|
|
30
|
+
$this->end = $end;
|
|
31
|
+
|
|
32
|
+
return $this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a [start, end] tuple from a `period` shorthand or explicit dates,
|
|
37
|
+
* falling back to a 30-day window. Shared by every analytics call.
|
|
38
|
+
*/
|
|
39
|
+
public static function resolvePeriod(?string $period, ?\DateTimeInterface $start, ?\DateTimeInterface $end, int $defaultDays = 30): array
|
|
40
|
+
{
|
|
41
|
+
if ($period !== null && $period !== '') {
|
|
42
|
+
$days = match ($period) {
|
|
43
|
+
'7d' => 7,
|
|
44
|
+
'14d' => 14,
|
|
45
|
+
'30d' => 30,
|
|
46
|
+
'90d' => 90,
|
|
47
|
+
'180d' => 180,
|
|
48
|
+
'365d' => 365,
|
|
49
|
+
default => null,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if ($days !== null) {
|
|
53
|
+
return [
|
|
54
|
+
Carbon::now()->subDays($days)->toDateTime(),
|
|
55
|
+
Carbon::now()->toDateTime(),
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return [
|
|
61
|
+
$start ?? Carbon::now()->subDays($defaultDays)->toDateTime(),
|
|
62
|
+
$end ?? Carbon::now()->toDateTime(),
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected function setCompany(Company $company): static
|
|
67
|
+
{
|
|
68
|
+
$this->company = $company;
|
|
69
|
+
|
|
70
|
+
return $this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected function companyCurrency(): string
|
|
74
|
+
{
|
|
75
|
+
return $this->company->currency ?? 'USD';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Analytics;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\FuelReport;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
7
|
+
use Illuminate\Support\Carbon;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Dual-axis fuel chart: weekly total cost (bars) + cost-per-km (line).
|
|
11
|
+
* Cost-per-km is sum(FuelReport.amount in primary currency) divided by
|
|
12
|
+
* sum(Order.distance, in km) for that bucket.
|
|
13
|
+
*/
|
|
14
|
+
class FuelEfficiency extends AbstractAnalytics
|
|
15
|
+
{
|
|
16
|
+
public function get(): array
|
|
17
|
+
{
|
|
18
|
+
$currency = $this->companyCurrency();
|
|
19
|
+
$start = $this->start ?? Carbon::now()->subDays(90)->toDateTime();
|
|
20
|
+
$end = $this->end ?? Carbon::now()->toDateTime();
|
|
21
|
+
|
|
22
|
+
$fuelByWeek = FuelReport::where('company_uuid', $this->company->uuid)
|
|
23
|
+
->where('currency', $currency)
|
|
24
|
+
->whereBetween('created_at', [$start, $end])
|
|
25
|
+
->selectRaw("YEARWEEK(created_at, 1) as wk, MIN(DATE(created_at)) as wk_start, SUM(amount) as total_cost")
|
|
26
|
+
->groupBy('wk')
|
|
27
|
+
->orderBy('wk')
|
|
28
|
+
->get();
|
|
29
|
+
|
|
30
|
+
$distanceByWeek = Order::where('company_uuid', $this->company->uuid)
|
|
31
|
+
->where('status', 'completed')
|
|
32
|
+
->whereBetween('updated_at', [$start, $end])
|
|
33
|
+
->selectRaw("YEARWEEK(updated_at, 1) as wk, SUM(distance) as total_distance")
|
|
34
|
+
->groupBy('wk')
|
|
35
|
+
->orderBy('wk')
|
|
36
|
+
->pluck('total_distance', 'wk');
|
|
37
|
+
|
|
38
|
+
$labels = [];
|
|
39
|
+
$costData = [];
|
|
40
|
+
$cpkmData = [];
|
|
41
|
+
|
|
42
|
+
foreach ($fuelByWeek as $row) {
|
|
43
|
+
$labels[] = Carbon::parse($row->wk_start)->format('M j');
|
|
44
|
+
$cost = (float) $row->total_cost;
|
|
45
|
+
$distanceM = (float) ($distanceByWeek[$row->wk] ?? 0);
|
|
46
|
+
$distanceKm = $distanceM / 1000.0;
|
|
47
|
+
$costData[] = round($cost, 2);
|
|
48
|
+
$cpkmData[] = $distanceKm > 0 ? round($cost / $distanceKm, 3) : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
$totalCost = array_sum($costData);
|
|
52
|
+
$totalDistanceM = array_sum($distanceByWeek->all());
|
|
53
|
+
$avgCostPerKm = $totalDistanceM > 0
|
|
54
|
+
? round($totalCost / ($totalDistanceM / 1000.0), 3)
|
|
55
|
+
: 0.0;
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
'labels' => $labels,
|
|
59
|
+
'datasets' => [
|
|
60
|
+
[
|
|
61
|
+
'type' => 'bar',
|
|
62
|
+
'label' => 'Fuel Cost',
|
|
63
|
+
'data' => $costData,
|
|
64
|
+
'yAxisID' => 'y1',
|
|
65
|
+
'backgroundColor' => '#3485e2',
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
'type' => 'line',
|
|
69
|
+
'label' => 'Cost per km',
|
|
70
|
+
'data' => $cpkmData,
|
|
71
|
+
'yAxisID' => 'y2',
|
|
72
|
+
'borderColor' => '#f59e0b',
|
|
73
|
+
'tension' => 0.3,
|
|
74
|
+
],
|
|
75
|
+
],
|
|
76
|
+
'summary' => [
|
|
77
|
+
'total_cost' => round($totalCost, 2),
|
|
78
|
+
'currency' => $currency,
|
|
79
|
+
'avg_cost_per_km' => $avgCostPerKm,
|
|
80
|
+
],
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Analytics;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\GeofenceEventLog;
|
|
6
|
+
use Illuminate\Support\Carbon;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Geofence dwell + violation summary. Today's violation count, period total,
|
|
10
|
+
* top-N dwell outliers (driver × zone × duration), and a bar chart of events
|
|
11
|
+
* by zone.
|
|
12
|
+
*/
|
|
13
|
+
class GeofenceViolations extends AbstractAnalytics
|
|
14
|
+
{
|
|
15
|
+
public function get(): array
|
|
16
|
+
{
|
|
17
|
+
$companyUuid = $this->company->uuid;
|
|
18
|
+
$start = $this->start ?? Carbon::now()->subDays(7)->toDateTime();
|
|
19
|
+
$end = $this->end ?? Carbon::now()->toDateTime();
|
|
20
|
+
|
|
21
|
+
$violationsToday = GeofenceEventLog::where('company_uuid', $companyUuid)
|
|
22
|
+
->where('event_type', 'dwelled')
|
|
23
|
+
->whereDate('occurred_at', Carbon::today())
|
|
24
|
+
->count();
|
|
25
|
+
|
|
26
|
+
$violationsPeriod = GeofenceEventLog::where('company_uuid', $companyUuid)
|
|
27
|
+
->where('event_type', 'dwelled')
|
|
28
|
+
->whereBetween('occurred_at', [$start, $end])
|
|
29
|
+
->count();
|
|
30
|
+
|
|
31
|
+
$topDwells = GeofenceEventLog::where('company_uuid', $companyUuid)
|
|
32
|
+
->where('event_type', 'dwelled')
|
|
33
|
+
->whereBetween('occurred_at', [$start, $end])
|
|
34
|
+
->whereNotNull('dwell_duration_minutes')
|
|
35
|
+
->orderByDesc('dwell_duration_minutes')
|
|
36
|
+
->limit(8)
|
|
37
|
+
->get(['subject_name', 'driver_uuid', 'geofence_name', 'dwell_duration_minutes', 'occurred_at']);
|
|
38
|
+
|
|
39
|
+
$byZoneRows = GeofenceEventLog::where('company_uuid', $companyUuid)
|
|
40
|
+
->whereBetween('occurred_at', [$start, $end])
|
|
41
|
+
->selectRaw('geofence_name, COUNT(*) as total')
|
|
42
|
+
->groupBy('geofence_name')
|
|
43
|
+
->orderByRaw('total DESC')
|
|
44
|
+
->limit(8)
|
|
45
|
+
->get();
|
|
46
|
+
|
|
47
|
+
return [
|
|
48
|
+
'violations_today' => $violationsToday,
|
|
49
|
+
'violations_period' => $violationsPeriod,
|
|
50
|
+
'top_dwells' => $topDwells->map(fn ($e) => [
|
|
51
|
+
'driver_uuid' => $e->driver_uuid,
|
|
52
|
+
'driver_name' => $e->subject_name,
|
|
53
|
+
'zone_name' => $e->geofence_name,
|
|
54
|
+
'duration_minutes' => (int) $e->dwell_duration_minutes,
|
|
55
|
+
'occurred_at' => $e->occurred_at,
|
|
56
|
+
])->all(),
|
|
57
|
+
'by_zone' => [
|
|
58
|
+
'labels' => $byZoneRows->pluck('geofence_name')->map(fn ($n) => $n ?: 'Unnamed')->all(),
|
|
59
|
+
'data' => $byZoneRows->pluck('total')->map(fn ($v) => (int) $v)->all(),
|
|
60
|
+
],
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
}
|