@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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { inject as service } from '@ember/service';
|
|
4
|
+
import { task } from 'ember-concurrency';
|
|
5
|
+
|
|
6
|
+
const CATEGORY_PALETTE = ['#3485e2', '#f59e0b', '#22c55e', '#ef4444', '#8b5cf6', '#a3a3a3', '#0ea5e9', '#ec4899'];
|
|
7
|
+
|
|
8
|
+
export default class WidgetIssuesInsightsComponent extends Component {
|
|
9
|
+
static widgetId = 'fleet-ops-issues-insights-widget';
|
|
10
|
+
|
|
11
|
+
@service fetch;
|
|
12
|
+
|
|
13
|
+
@tracked data = null;
|
|
14
|
+
@tracked error = null;
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
super(...arguments);
|
|
18
|
+
this.load.perform();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get categoryDatasets() {
|
|
22
|
+
if (!this.data?.by_category?.data?.length) return null;
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
data: this.data.by_category.data,
|
|
26
|
+
backgroundColor: this.data.by_category.data.map((_, i) => CATEGORY_PALETTE[i % CATEGORY_PALETTE.length]),
|
|
27
|
+
borderWidth: 0,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get categoryLabels() {
|
|
33
|
+
return this.data?.by_category?.labels ?? [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get categoryOptions() {
|
|
37
|
+
return {
|
|
38
|
+
responsive: true,
|
|
39
|
+
maintainAspectRatio: false,
|
|
40
|
+
plugins: {
|
|
41
|
+
legend: { position: 'bottom', labels: { boxWidth: 10, padding: 6, font: { size: 10 } } },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get resolutionDisplay() {
|
|
47
|
+
const hours = this.data?.avg_resolution_hours;
|
|
48
|
+
if (typeof hours !== 'number') return '—';
|
|
49
|
+
if (hours < 1) return `${Math.round(hours * 60)}m`;
|
|
50
|
+
if (hours < 48) return `${hours}h`;
|
|
51
|
+
return `${(hours / 24).toFixed(1)}d`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@task *load() {
|
|
55
|
+
try {
|
|
56
|
+
this.data = yield this.fetch.get('fleet-ops/analytics/issues-insights', { period: '30d' });
|
|
57
|
+
this.error = null;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
this.error = e?.message ?? 'Failed to load issues';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{{!--
|
|
2
|
+
KPI tile. Layout strategy:
|
|
3
|
+
- Title + icon at the top.
|
|
4
|
+
- Value + delta directly under the title.
|
|
5
|
+
- Sparkline absolutely positioned at the bottom edge of the card so every
|
|
6
|
+
tile lines up regardless of value-string length, and a flat-zero
|
|
7
|
+
sparkline (all-zero data, fill:'origin', y.min=0) sits flush along the
|
|
8
|
+
bottom rather than floating in the middle.
|
|
9
|
+
--}}
|
|
10
|
+
<div class="fleet-ops-widget fleet-ops-kpi-tile fleet-ops-kpi-tile-pop {{this.cardAccentClass}} relative flex flex-col h-full rounded-md border shadow-sm overflow-hidden {{@class}}" ...attributes>
|
|
11
|
+
<div class="flex items-center justify-between px-2.5 pt-2 relative z-10">
|
|
12
|
+
<span class="text-[10px] uppercase tracking-wider text-gray-500 dark:text-gray-400 font-bold truncate">
|
|
13
|
+
{{@title}}
|
|
14
|
+
</span>
|
|
15
|
+
{{#if @icon}}
|
|
16
|
+
<FaIcon @icon={{@icon}} @prefix={{or @iconPrefix "fas"}} class="kpi-tile-icon text-gray-400 dark:text-gray-500 text-[11px]" />
|
|
17
|
+
{{/if}}
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="flex-1 flex flex-col px-2.5 pt-1 min-h-0 relative z-10">
|
|
21
|
+
{{#if this.load.isRunning}}
|
|
22
|
+
<div class="flex-1 flex items-center justify-center">
|
|
23
|
+
<Spinner />
|
|
24
|
+
</div>
|
|
25
|
+
{{else if this.error}}
|
|
26
|
+
<div class="flex-1 flex items-center justify-center text-[10px] text-red-500 dark:text-red-400">
|
|
27
|
+
{{this.error}}
|
|
28
|
+
</div>
|
|
29
|
+
{{else if this.data}}
|
|
30
|
+
<div class="flex items-baseline gap-1.5 kpi-tile-value-row">
|
|
31
|
+
<span class="text-2xl font-bold leading-none truncate {{this.valueTextClass}}">
|
|
32
|
+
{{#if (eq this.format "money")}}
|
|
33
|
+
{{format-currency this.data.value this.currency}}
|
|
34
|
+
{{else if (eq this.format "meters")}}
|
|
35
|
+
{{format-meters this.data.value}}
|
|
36
|
+
{{else if (eq this.format "duration")}}
|
|
37
|
+
{{format-duration this.data.value}}
|
|
38
|
+
{{else}}
|
|
39
|
+
{{this.data.value}}
|
|
40
|
+
{{/if}}
|
|
41
|
+
</span>
|
|
42
|
+
{{#if this.hasDelta}}
|
|
43
|
+
<Badge @status={{this.deltaStatus}} @text={{this.deltaText}} @hideIcon={{true}} @disableHumanize={{true}} @wrapperClass="text-[9px] px-1 py-0 font-bold" />
|
|
44
|
+
{{/if}}
|
|
45
|
+
</div>
|
|
46
|
+
{{/if}}
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{{#if this.sparklineDatasets}}
|
|
50
|
+
<div class="kpi-tile-sparkline absolute inset-x-0 bottom-0">
|
|
51
|
+
<Chart
|
|
52
|
+
@type="line"
|
|
53
|
+
@labels={{this.sparklineLabels}}
|
|
54
|
+
@datasets={{this.sparklineDatasets}}
|
|
55
|
+
@options={{this.sparklineOptions}}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
{{/if}}
|
|
59
|
+
</div>
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { action } from '@ember/object';
|
|
4
|
+
import { inject as service } from '@ember/service';
|
|
5
|
+
import { task } from 'ember-concurrency';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* <Widget::KpiTile @slug="earnings" @title="..." @format="money" @showDelta={{true}}
|
|
9
|
+
* @showSparkline={{true}} @deltaInverse={{false}} @period="30d" />
|
|
10
|
+
*
|
|
11
|
+
* Renders a single KPI tile backed by `/fleet-ops/metrics/{slug}`. Hosts the
|
|
12
|
+
* format-specific value rendering, the optional period-over-period delta
|
|
13
|
+
* badge (with `deltaInverse` to flip semantics for inverse-good metrics like
|
|
14
|
+
* open_issues), and the optional sparkline line chart.
|
|
15
|
+
*/
|
|
16
|
+
export default class WidgetKpiTileComponent extends Component {
|
|
17
|
+
@service fetch;
|
|
18
|
+
@tracked data = null;
|
|
19
|
+
@tracked error = null;
|
|
20
|
+
|
|
21
|
+
get period() {
|
|
22
|
+
return this.args.period ?? '30d';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get showDelta() {
|
|
26
|
+
return this.args.showDelta ?? true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get showSparkline() {
|
|
30
|
+
return this.args.showSparkline ?? true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get deltaInverse() {
|
|
34
|
+
return this.args.deltaInverse ?? false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get format() {
|
|
38
|
+
return this.args.format ?? this.data?.format ?? 'count';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get currency() {
|
|
42
|
+
return this.data?.currency ?? 'USD';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get deltaPct() {
|
|
46
|
+
return this.data?.delta_pct;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get hasDelta() {
|
|
50
|
+
return this.showDelta && typeof this.deltaPct === 'number';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get deltaStatus() {
|
|
54
|
+
const pct = this.deltaPct;
|
|
55
|
+
if (typeof pct !== 'number' || pct === 0) return 'info';
|
|
56
|
+
const positive = pct > 0;
|
|
57
|
+
const isGood = this.deltaInverse ? !positive : positive;
|
|
58
|
+
return isGood ? 'success' : 'danger';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get deltaText() {
|
|
62
|
+
const pct = this.deltaPct;
|
|
63
|
+
const sign = pct > 0 ? '+' : '';
|
|
64
|
+
return `${sign}${pct}%`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Tints the card subtly based on trend direction so the dashboard pops. */
|
|
68
|
+
get cardAccentClass() {
|
|
69
|
+
const pct = this.deltaPct;
|
|
70
|
+
if (typeof pct !== 'number' || pct === 0) {
|
|
71
|
+
return 'kpi-accent-neutral';
|
|
72
|
+
}
|
|
73
|
+
const positive = pct > 0;
|
|
74
|
+
const isGood = this.deltaInverse ? !positive : positive;
|
|
75
|
+
return isGood ? 'kpi-accent-good' : 'kpi-accent-bad';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get valueTextClass() {
|
|
79
|
+
const pct = this.deltaPct;
|
|
80
|
+
if (typeof pct !== 'number' || pct === 0) {
|
|
81
|
+
return 'text-black dark:text-gray-100';
|
|
82
|
+
}
|
|
83
|
+
const positive = pct > 0;
|
|
84
|
+
const isGood = this.deltaInverse ? !positive : positive;
|
|
85
|
+
return isGood ? 'text-emerald-600 dark:text-emerald-400' : 'text-rose-600 dark:text-rose-400';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Neon palette per delta direction. Drives line + fill + value highlight. */
|
|
89
|
+
get accentColors() {
|
|
90
|
+
// Bright neon-ish trio that pops on the dark theme without being garish in light.
|
|
91
|
+
const good = { line: '#22c55e', fill: 'rgba(34, 197, 94, 0.22)', glow: 'rgba(34, 197, 94, 0.35)' };
|
|
92
|
+
const bad = { line: '#ef4444', fill: 'rgba(239, 68, 68, 0.22)', glow: 'rgba(239, 68, 68, 0.35)' };
|
|
93
|
+
const neutral = { line: '#3485e2', fill: 'rgba(52, 133, 226, 0.18)', glow: 'rgba(52, 133, 226, 0.30)' };
|
|
94
|
+
|
|
95
|
+
const pct = this.deltaPct;
|
|
96
|
+
if (typeof pct !== 'number' || pct === 0) return neutral;
|
|
97
|
+
const positive = pct > 0;
|
|
98
|
+
const isGood = this.deltaInverse ? !positive : positive;
|
|
99
|
+
return isGood ? good : bad;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get sparklineDatasets() {
|
|
103
|
+
const points = this.data?.sparkline?.data;
|
|
104
|
+
if (!points || points.length === 0) return null;
|
|
105
|
+
|
|
106
|
+
const { line, fill } = this.accentColors;
|
|
107
|
+
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
data: points,
|
|
111
|
+
borderColor: line,
|
|
112
|
+
backgroundColor: fill,
|
|
113
|
+
pointRadius: 0,
|
|
114
|
+
borderWidth: 1.75,
|
|
115
|
+
tension: 0.4,
|
|
116
|
+
// Anchor the area fill to the chart's y=0 axis so the sparkline
|
|
117
|
+
// visually sits at the BOTTOM of the card regardless of the data range.
|
|
118
|
+
fill: 'origin',
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get sparklineLabels() {
|
|
124
|
+
return this.data?.sparkline?.labels ?? [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get sparklineOptions() {
|
|
128
|
+
return {
|
|
129
|
+
responsive: true,
|
|
130
|
+
maintainAspectRatio: false,
|
|
131
|
+
animation: { duration: 600, easing: 'easeOutQuart' },
|
|
132
|
+
plugins: {
|
|
133
|
+
legend: { display: false },
|
|
134
|
+
tooltip: { enabled: false },
|
|
135
|
+
},
|
|
136
|
+
scales: {
|
|
137
|
+
x: { display: false },
|
|
138
|
+
// Lock min to 0 so flat-zero data sits flush along the bottom edge.
|
|
139
|
+
// suggestedMin/Max keep the line off the very top/bottom for non-zero data.
|
|
140
|
+
y: { display: false, min: 0, beginAtZero: true, grace: '5%' },
|
|
141
|
+
},
|
|
142
|
+
elements: { line: { borderJoinStyle: 'round' } },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
constructor() {
|
|
147
|
+
super(...arguments);
|
|
148
|
+
this.load.perform();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@task *load() {
|
|
152
|
+
try {
|
|
153
|
+
this.data = yield this.fetch.get(`fleet-ops/metrics/${this.args.slug}`, {
|
|
154
|
+
period: this.period,
|
|
155
|
+
sparkline: this.showSparkline,
|
|
156
|
+
compare: this.showDelta,
|
|
157
|
+
});
|
|
158
|
+
this.error = null;
|
|
159
|
+
} catch (e) {
|
|
160
|
+
this.error = e?.message ?? 'Failed to load metric';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@action
|
|
165
|
+
refresh() {
|
|
166
|
+
this.load.perform();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<div class="fleet-ops-widget fleet-ops-live-fleet flex flex-col h-full {{@class}}" ...attributes>
|
|
2
|
+
<div class="flex items-center justify-between px-3 py-1.5 border dark:border-gray-700 border-gray-200 dark:bg-gray-800 bg-gray-50 rounded-t-md shadow-sm">
|
|
3
|
+
<span class="text-xs font-bold text-black dark:text-gray-100">{{t "widget.live-fleet.title"}}</span>
|
|
4
|
+
<div class="flex items-center gap-3 text-[10px] text-gray-500 dark:text-gray-400">
|
|
5
|
+
<span class="flex items-center" title="Drivers"><FaIcon @icon="id-card" class="mr-1" />{{this.drivers.length}}</span>
|
|
6
|
+
<span class="flex items-center" title="Vehicles"><FaIcon @icon="truck" class="mr-1" />{{this.vehicles.length}}</span>
|
|
7
|
+
<span class="flex items-center" title="Active orders"><FaIcon @icon="bolt" class="mr-1" />{{this.activeOrderCount}}</span>
|
|
8
|
+
<button type="button" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" {{on "click" this.refresh}}>
|
|
9
|
+
<FaIcon @icon="rotate" class={{if this.load.isRunning "animate-spin"}} />
|
|
10
|
+
</button>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="flex-1 relative border border-t-0 dark:border-gray-700 border-gray-200 rounded-b-md overflow-hidden min-h-0 bg-gray-100 dark:bg-gray-900">
|
|
15
|
+
<div class="fleetbase-leaflet-map-container w-full h-full" {{set-container-dimensions}}>
|
|
16
|
+
<LeafletMap
|
|
17
|
+
class="w-full h-full"
|
|
18
|
+
@lat={{get this.center 0}}
|
|
19
|
+
@lng={{get this.center 1}}
|
|
20
|
+
@zoom={{this.zoom}}
|
|
21
|
+
@zoomControl={{true}}
|
|
22
|
+
@onLoad={{this.onMapLoad}}
|
|
23
|
+
{{set-container-dimensions}}
|
|
24
|
+
as |layers|
|
|
25
|
+
>
|
|
26
|
+
<layers.tile @url={{this.tileUrl}} />
|
|
27
|
+
|
|
28
|
+
{{#if this.load.isIdle}}
|
|
29
|
+
{{! Drivers — fleetops' rotating tracking-marker matches the operational
|
|
30
|
+
live-map's look and animates on heading changes. }}
|
|
31
|
+
{{#each this.drivers as |driver|}}
|
|
32
|
+
<layers.tracking-marker
|
|
33
|
+
@id={{driver.uuid}}
|
|
34
|
+
@publicId={{driver.public_id}}
|
|
35
|
+
@location={{array driver.lat driver.lng}}
|
|
36
|
+
@rotationAngle={{driver.heading}}
|
|
37
|
+
@icon={{icon iconUrl=(or driver.vehicle_avatar driver.avatar_url "/engines-dist/images/map-marker.png") iconSize=(array 28 28) iconAnchor=(array 14 28)}}
|
|
38
|
+
@draggable={{false}}
|
|
39
|
+
as |marker|
|
|
40
|
+
>
|
|
41
|
+
<marker.popup @maxWidth="240" @minWidth="180">
|
|
42
|
+
<div class="flex flex-row gap-2">
|
|
43
|
+
<div class="w-9 h-9 flex-shrink-0">
|
|
44
|
+
<img src={{or driver.avatar_url driver.vehicle_avatar}} alt={{driver.name}} class="rounded-md w-9 h-9 object-cover shadow-sm" />
|
|
45
|
+
</div>
|
|
46
|
+
<div class="flex-1 min-w-0">
|
|
47
|
+
<div class="text-xs font-semibold truncate">{{or driver.name "Driver"}}</div>
|
|
48
|
+
<div class="text-[11px]">
|
|
49
|
+
{{#if driver.current_order_uuid}}
|
|
50
|
+
<span class="text-blue-500"><FaIcon @icon="bolt" class="mr-1" />On order</span>
|
|
51
|
+
{{else if driver.online}}
|
|
52
|
+
<span class="text-green-500"><FaIcon @icon="circle" @size="2xs" class="mr-1" />Online</span>
|
|
53
|
+
{{else}}
|
|
54
|
+
<span class="text-gray-400"><FaIcon @icon="circle" @size="2xs" class="mr-1" />Offline</span>
|
|
55
|
+
{{/if}}
|
|
56
|
+
</div>
|
|
57
|
+
<div class="text-[10px] text-gray-400 mt-0.5 truncate">{{driver.lat}}, {{driver.lng}}</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</marker.popup>
|
|
61
|
+
<marker.tooltip @permanent={{false}} @sticky={{true}} @direction="top">
|
|
62
|
+
<div class="text-[11px] font-semibold">{{or driver.name "Driver"}}</div>
|
|
63
|
+
</marker.tooltip>
|
|
64
|
+
</layers.tracking-marker>
|
|
65
|
+
{{/each}}
|
|
66
|
+
|
|
67
|
+
{{! Vehicles — also rendered via tracking-marker so the heading
|
|
68
|
+
animation applies on telematics-driven position updates. }}
|
|
69
|
+
{{#each this.vehicles as |vehicle|}}
|
|
70
|
+
<layers.tracking-marker
|
|
71
|
+
@id={{vehicle.uuid}}
|
|
72
|
+
@publicId={{vehicle.public_id}}
|
|
73
|
+
@location={{array vehicle.lat vehicle.lng}}
|
|
74
|
+
@rotationAngle={{vehicle.heading}}
|
|
75
|
+
@icon={{icon iconUrl=(or vehicle.avatar_url vehicle.photo_url "/engines-dist/images/map-marker.png") iconSize=(array 28 28) iconAnchor=(array 14 28)}}
|
|
76
|
+
@draggable={{false}}
|
|
77
|
+
as |marker|
|
|
78
|
+
>
|
|
79
|
+
<marker.popup @maxWidth="240" @minWidth="180">
|
|
80
|
+
<div class="flex flex-row gap-2">
|
|
81
|
+
<div class="w-9 h-9 flex-shrink-0">
|
|
82
|
+
<img src={{or vehicle.photo_url vehicle.avatar_url}} alt={{vehicle.name}} class="rounded-md w-9 h-9 object-cover shadow-sm" />
|
|
83
|
+
</div>
|
|
84
|
+
<div class="flex-1 min-w-0">
|
|
85
|
+
<div class="text-xs font-semibold truncate">{{or vehicle.name "Vehicle"}}</div>
|
|
86
|
+
{{#if vehicle.plate_number}}
|
|
87
|
+
<div class="text-[10px] text-gray-500 truncate">{{vehicle.plate_number}}</div>
|
|
88
|
+
{{/if}}
|
|
89
|
+
{{#if vehicle.driver_name}}
|
|
90
|
+
<div class="text-[11px] text-gray-600 dark:text-gray-300 truncate">
|
|
91
|
+
<FaIcon @icon="id-card" class="mr-1 text-gray-400" />{{vehicle.driver_name}}
|
|
92
|
+
</div>
|
|
93
|
+
{{/if}}
|
|
94
|
+
<div class="text-[10px] text-gray-400 mt-0.5 truncate">{{vehicle.lat}}, {{vehicle.lng}}</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</marker.popup>
|
|
98
|
+
<marker.tooltip @permanent={{false}} @sticky={{true}} @direction="top">
|
|
99
|
+
<div class="text-[11px] font-semibold">{{or vehicle.name "Vehicle"}}</div>
|
|
100
|
+
</marker.tooltip>
|
|
101
|
+
</layers.tracking-marker>
|
|
102
|
+
{{/each}}
|
|
103
|
+
{{/if}}
|
|
104
|
+
</LeafletMap>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{{#if this.load.isRunning}}
|
|
108
|
+
<div class="absolute inset-0 flex items-center justify-center bg-white/70 dark:bg-gray-900/70 z-[400]">
|
|
109
|
+
<Spinner />
|
|
110
|
+
</div>
|
|
111
|
+
{{else if this.error}}
|
|
112
|
+
<div class="absolute inset-0 flex items-center justify-center text-xs text-red-500 z-[400]">{{this.error}}</div>
|
|
113
|
+
{{else if (not this.totalMarkerCount)}}
|
|
114
|
+
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
115
|
+
<div class="text-center text-[11px] text-gray-500 dark:text-gray-400 bg-white/90 dark:bg-gray-900/90 rounded-md px-3 py-2 shadow-sm">
|
|
116
|
+
<FaIcon @icon="map-location-dot" class="text-xl block mb-1 opacity-50" />
|
|
117
|
+
No active drivers or vehicles to display.
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
{{/if}}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|