@fleetbase/fleetops-engine 0.6.56 → 0.6.58

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.
Files changed (82) hide show
  1. package/addon/components/ai/create-order-preview.hbs +297 -0
  2. package/addon/components/ai/create-order-preview.js +362 -0
  3. package/addon/components/ai/route-preview-map.hbs +79 -0
  4. package/addon/components/ai/route-preview-map.js +527 -0
  5. package/addon/components/fleet/details.hbs +21 -8
  6. package/addon/components/fleet/details.js +55 -1
  7. package/addon/components/fleet/hierarchy-tree.hbs +137 -0
  8. package/addon/components/fleet/hierarchy-tree.js +528 -0
  9. package/addon/components/map/drawer/device-event-listing.js +1 -0
  10. package/addon/components/map/drawer/driver-listing.js +1 -0
  11. package/addon/components/map/drawer/place-listing.js +1 -0
  12. package/addon/components/map/drawer/vehicle-listing.js +1 -0
  13. package/addon/components/order/details.hbs +34 -1
  14. package/addon/components/order/form/details.hbs +4 -0
  15. package/addon/components/order/form.hbs +9 -1
  16. package/addon/components/vehicle/card.hbs +14 -2
  17. package/addon/components/vehicle/form.hbs +13 -1
  18. package/addon/controllers/settings/map.js +4 -0
  19. package/addon/extension.js +14 -0
  20. package/addon/routes/management/fleets/index/details.js +1 -1
  21. package/addon/services/map-adapter/google.js +56 -16
  22. package/addon/services/map-manager.js +12 -0
  23. package/addon/styles/fleetops-engine.css +487 -1
  24. package/addon/utils/map-drawer-dropdown-position.js +11 -13
  25. package/app/components/ai/create-order-preview.js +1 -0
  26. package/app/components/ai/route-preview-map.js +1 -0
  27. package/app/components/fleet/hierarchy-tree.js +1 -0
  28. package/app/services/route-engine.js +1 -0
  29. package/composer.json +9 -2
  30. package/extension.json +1 -1
  31. package/package.json +6 -4
  32. package/phpunit.xml.dist +4 -4
  33. package/pnpm-workspace.yaml +2 -1
  34. package/scripts/coverage-runner.php +51 -0
  35. package/scripts/coverage-summary.php +110 -0
  36. package/scripts/normalize-rgba-alpha.js +90 -0
  37. package/scripts/pest-bootstrap.php +177 -0
  38. package/scripts/pest-file-runner.php +82 -0
  39. package/scripts/pest-runner.php +64 -0
  40. package/server/src/Http/Resources/v1/Fleet.php +8 -2
  41. package/server/src/Models/Driver.php +6 -0
  42. package/server/src/Models/Order.php +18 -6
  43. package/server/src/Models/Place.php +11 -3
  44. package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +3 -1
  45. package/server/src/Providers/FleetOpsServiceProvider.php +26 -0
  46. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +88 -0
  47. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +108 -0
  48. package/server/src/Support/Ai/Capabilities/ConsoleNavigationCapability.php +55 -0
  49. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +508 -0
  50. package/server/src/Support/Ai/Capabilities/DocsHelpCapability.php +49 -0
  51. package/server/src/Support/Ai/Capabilities/ImportOrdersPreviewCapability.php +66 -0
  52. package/server/src/Support/Ai/Capabilities/OperationalQueryCapability.php +319 -0
  53. package/server/src/Support/Ai/Capabilities/OptimizeOrderRouteCapability.php +240 -0
  54. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +95 -0
  55. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +195 -0
  56. package/server/src/Support/Ai/FleetOpsAiQueryResources.php +150 -0
  57. package/server/src/Support/Utils.php +52 -0
  58. package/server/src/Tracking/TrackingOptions.php +6 -1
  59. package/server/src/routes.php +0 -11
  60. package/server/tests/AiCreateOrderPreviewCapabilityTest.php +131 -0
  61. package/server/tests/AiOperationalQueryCapabilityTest.php +75 -0
  62. package/server/tests/AnalyticsRoutesTest.php +5 -3
  63. package/server/tests/CustomerEndpointTest.php +1 -0
  64. package/server/tests/DeviceFilterTest.php +1 -2
  65. package/server/tests/FleetHierarchyResourceTest.php +12 -0
  66. package/server/tests/FuelProviderRegistryTest.php +7 -9
  67. package/server/tests/LiveControllerViewportTest.php +8 -16
  68. package/server/tests/LiveOrderQueryTest.php +18 -24
  69. package/server/tests/OrderPayloadPlaceResolutionTest.php +53 -0
  70. package/server/tests/PointResolutionTest.php +63 -0
  71. package/server/tests/ServiceAreaZoneApiShapeTest.php +8 -9
  72. package/server/tests/ServiceRateAlgorithmWeightVariablesTest.php +1 -1
  73. package/server/tests/TelematicsHardeningTest.php +31 -26
  74. package/server/tests/TrackingIntelligenceTest.php +65 -25
  75. package/server/tests/VehicleDriverAssignmentTest.php +8 -9
  76. package/addon/components/table/cell/dropdown/action-item.hbs +0 -31
  77. package/addon/components/table/cell/dropdown/action-item.js +0 -62
  78. package/addon/components/table/cell/dropdown.hbs +0 -28
  79. package/addon/components/table/cell/dropdown.js +0 -91
  80. package/app/components/table/cell/dropdown/action-item.js +0 -1
  81. package/app/components/table/cell/dropdown.js +0 -1
  82. package/server/src/Http/Controllers/Api/v1/OrganizationController.php +0 -45
@@ -0,0 +1,51 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ $args = array_slice($argv, 1);
6
+
7
+ if ($args === []) {
8
+ fwrite(STDERR, "Usage: php scripts/coverage-runner.php <pest coverage arguments>\n");
9
+ exit(1);
10
+ }
11
+
12
+ $pestRunner = getcwd() . '/scripts/pest-runner.php';
13
+ if (!is_file($pestRunner)) {
14
+ fwrite(STDERR, "Unable to find Pest runner at scripts/pest-runner.php.\n");
15
+ exit(1);
16
+ }
17
+
18
+ $hasCoverageExtension = extension_loaded('xdebug') || extension_loaded('pcov');
19
+
20
+ if (!$hasCoverageExtension) {
21
+ fwrite(STDERR, "No PHP coverage driver is available.\n\n");
22
+ fwrite(STDERR, "Install or enable one of:\n");
23
+ fwrite(STDERR, " - Xdebug with XDEBUG_MODE=coverage\n");
24
+ fwrite(STDERR, " - PCOV\n");
25
+ fwrite(STDERR, "\n");
26
+ fwrite(STDERR, 'Current PHP binary: ' . PHP_BINARY . "\n");
27
+ exit(1);
28
+ }
29
+
30
+ putenv('XDEBUG_MODE=coverage');
31
+ $_ENV['XDEBUG_MODE'] = 'coverage';
32
+ $_SERVER['XDEBUG_MODE'] = 'coverage';
33
+
34
+ $hasTestTarget = false;
35
+ foreach ($args as $arg) {
36
+ if ($arg !== '' && $arg[0] !== '-') {
37
+ $hasTestTarget = true;
38
+ break;
39
+ }
40
+ }
41
+
42
+ if (!$hasTestTarget && is_dir(getcwd() . '/server/tests')) {
43
+ $args[] = 'server/tests';
44
+ }
45
+
46
+ $command = array_merge([PHP_BINARY, $pestRunner], $args);
47
+ $escapedCommand = implode(' ', array_map('escapeshellarg', $command));
48
+
49
+ passthru($escapedCommand, $exitCode);
50
+
51
+ exit($exitCode);
@@ -0,0 +1,110 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ $cloverPath = $argv[1] ?? 'coverage/clover.xml';
6
+
7
+ if (!is_file($cloverPath)) {
8
+ fwrite(STDERR, "Coverage file not found: {$cloverPath}\n");
9
+ fwrite(STDERR, "Run `composer test:coverage:clover` first.\n");
10
+ exit(1);
11
+ }
12
+
13
+ $xml = simplexml_load_file($cloverPath);
14
+ if (!$xml) {
15
+ fwrite(STDERR, "Unable to parse Clover coverage file: {$cloverPath}\n");
16
+ exit(1);
17
+ }
18
+
19
+ function coveragePercent(int $covered, int $total): float
20
+ {
21
+ return $total === 0 ? 0.0 : round(($covered / $total) * 100, 2);
22
+ }
23
+
24
+ function intMetric(SimpleXMLElement $node, string $name): int
25
+ {
26
+ return (int) ($node->metrics[$name] ?? 0);
27
+ }
28
+
29
+ $project = $xml->project;
30
+ $metrics = $project->metrics;
31
+
32
+ $statements = (int) ($metrics['statements'] ?? 0);
33
+ $coveredStatements = (int) ($metrics['coveredstatements'] ?? 0);
34
+ $methods = (int) ($metrics['methods'] ?? 0);
35
+ $coveredMethods = (int) ($metrics['coveredmethods'] ?? 0);
36
+ $classes = (int) ($metrics['classes'] ?? 0);
37
+ $coveredClasses = (int) ($metrics['coveredclasses'] ?? 0);
38
+
39
+ $files = [];
40
+ $directories = [];
41
+
42
+ foreach ($project->xpath('.//file') ?: [] as $file) {
43
+ $path = (string) $file['name'];
44
+ $fileStatements = intMetric($file, 'statements');
45
+ $coveredFileLines = intMetric($file, 'coveredstatements');
46
+ $fileMethods = intMetric($file, 'methods');
47
+ $coveredFileMethod = intMetric($file, 'coveredmethods');
48
+
49
+ if ($fileStatements === 0) {
50
+ continue;
51
+ }
52
+
53
+ $files[] = [
54
+ 'path' => $path,
55
+ 'covered' => $coveredFileLines,
56
+ 'statements' => $fileStatements,
57
+ 'methods' => $fileMethods,
58
+ 'covered_methods' => $coveredFileMethod,
59
+ 'percent' => coveragePercent($coveredFileLines, $fileStatements),
60
+ ];
61
+
62
+ $relativePath = preg_replace('#^' . preg_quote(getcwd(), '#') . '/?#', '', $path);
63
+ $parts = explode('/', $relativePath ?: $path);
64
+ $directory = count($parts) > 2 ? $parts[0] . '/' . $parts[1] : dirname($relativePath ?: $path);
65
+
66
+ if (!isset($directories[$directory])) {
67
+ $directories[$directory] = [
68
+ 'covered' => 0,
69
+ 'statements' => 0,
70
+ ];
71
+ }
72
+
73
+ $directories[$directory]['covered'] += $coveredFileLines;
74
+ $directories[$directory]['statements'] += $fileStatements;
75
+ }
76
+
77
+ usort($files, function (array $a, array $b): int {
78
+ return $a['percent'] <=> $b['percent']
79
+ ?: $b['statements'] <=> $a['statements'];
80
+ });
81
+
82
+ $directoryRows = [];
83
+ foreach ($directories as $directory => $directoryMetrics) {
84
+ $directoryRows[] = [
85
+ 'directory' => $directory,
86
+ 'covered' => $directoryMetrics['covered'],
87
+ 'statements' => $directoryMetrics['statements'],
88
+ 'percent' => coveragePercent($directoryMetrics['covered'], $directoryMetrics['statements']),
89
+ ];
90
+ }
91
+
92
+ usort($directoryRows, function (array $a, array $b): int {
93
+ return $a['percent'] <=> $b['percent']
94
+ ?: $b['statements'] <=> $a['statements'];
95
+ });
96
+
97
+ printf("Line coverage: %.2f%% (%d/%d statements)\n", coveragePercent($coveredStatements, $statements), $coveredStatements, $statements);
98
+ printf("Method coverage: %.2f%% (%d/%d methods)\n", coveragePercent($coveredMethods, $methods), $coveredMethods, $methods);
99
+ printf("Class coverage: %.2f%% (%d/%d classes)\n", coveragePercent($coveredClasses, $classes), $coveredClasses, $classes);
100
+
101
+ echo "\nLowest covered directories:\n";
102
+ foreach (array_slice($directoryRows, 0, 10) as $row) {
103
+ printf(" %6.2f%% %5d/%-5d %s\n", $row['percent'], $row['covered'], $row['statements'], $row['directory']);
104
+ }
105
+
106
+ echo "\nLowest covered files:\n";
107
+ foreach (array_slice($files, 0, 20) as $file) {
108
+ $relativePath = preg_replace('#^' . preg_quote(getcwd(), '#') . '/?#', '', $file['path']);
109
+ printf(" %6.2f%% %5d/%-5d %s\n", $file['percent'], $file['covered'], $file['statements'], $relativePath ?: $file['path']);
110
+ }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const root = process.cwd();
8
+ const checkOnly = process.argv.includes('--check');
9
+ const cssRoot = path.join(root, 'addon');
10
+ const rgbaPercentAlphaPattern = /rgba\(\s*([+-]?(?:\d*\.)?\d+)\s*,\s*([+-]?(?:\d*\.)?\d+)\s*,\s*([+-]?(?:\d*\.)?\d+)\s*,\s*([+-]?(?:\d*\.)?\d+)%\s*\)/g;
11
+
12
+ function collectCssFiles(dir, files = []) {
13
+ if (!fs.existsSync(dir)) {
14
+ return files;
15
+ }
16
+
17
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
18
+ const fullPath = path.join(dir, entry.name);
19
+
20
+ if (entry.isDirectory()) {
21
+ collectCssFiles(fullPath, files);
22
+ continue;
23
+ }
24
+
25
+ if (entry.isFile() && entry.name.endsWith('.css')) {
26
+ files.push(fullPath);
27
+ }
28
+ }
29
+
30
+ return files;
31
+ }
32
+
33
+ function formatAlpha(percent) {
34
+ const alpha = Number(percent) / 100;
35
+ return Number.isInteger(alpha) ? String(alpha) : String(Number(alpha.toFixed(4)));
36
+ }
37
+
38
+ function lineForIndex(source, index) {
39
+ return source.slice(0, index).split('\n').length;
40
+ }
41
+
42
+ const changes = [];
43
+
44
+ for (const filePath of collectCssFiles(cssRoot)) {
45
+ const source = fs.readFileSync(filePath, 'utf8');
46
+ const matches = [];
47
+
48
+ const next = source.replace(rgbaPercentAlphaPattern, (match, red, green, blue, alphaPercent, offset) => {
49
+ const replacement = `rgba(${red}, ${green}, ${blue}, ${formatAlpha(alphaPercent)})`;
50
+ matches.push({
51
+ line: lineForIndex(source, offset),
52
+ before: match,
53
+ after: replacement,
54
+ });
55
+ return replacement;
56
+ });
57
+
58
+ if (matches.length === 0) {
59
+ continue;
60
+ }
61
+
62
+ changes.push({
63
+ filePath,
64
+ matches,
65
+ });
66
+
67
+ if (!checkOnly) {
68
+ fs.writeFileSync(filePath, next);
69
+ }
70
+ }
71
+
72
+ if (changes.length === 0) {
73
+ console.log('No comma-form rgba() percent-alpha values found.');
74
+ process.exit(0);
75
+ }
76
+
77
+ for (const change of changes) {
78
+ const relativePath = path.relative(root, change.filePath);
79
+
80
+ for (const match of change.matches) {
81
+ console.log(`${relativePath}:${match.line} ${match.before} -> ${match.after}`);
82
+ }
83
+ }
84
+
85
+ if (checkOnly) {
86
+ console.error('\nRun `npm run fix:css-alpha` to normalize these values before production builds.');
87
+ process.exit(1);
88
+ }
89
+
90
+ console.log(`\nNormalized ${changes.reduce((total, change) => total + change.matches.length, 0)} rgba() alpha value(s).`);
@@ -0,0 +1,177 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ $autoloadCandidates = [
6
+ getcwd() . '/server_vendor/autoload.php',
7
+ getcwd() . '/vendor/autoload.php',
8
+ ];
9
+
10
+ foreach ($autoloadCandidates as $candidate) {
11
+ if (is_file($candidate)) {
12
+ require_once $candidate;
13
+ break;
14
+ }
15
+ }
16
+
17
+ if (!function_exists('config')) {
18
+ function config(?string $key = null, mixed $default = null): mixed
19
+ {
20
+ return $default;
21
+ }
22
+ }
23
+
24
+ if (class_exists('Illuminate\Container\Container') && class_exists('Illuminate\Support\Facades\Facade')) {
25
+ $app = Illuminate\Container\Container::getInstance();
26
+ Illuminate\Support\Facades\Facade::setFacadeApplication($app);
27
+
28
+ if (!$app->bound('http') && class_exists('Illuminate\Http\Client\Factory')) {
29
+ $app->singleton('http', fn () => new Illuminate\Http\Client\Factory());
30
+ }
31
+
32
+ if (!$app->bound('cache') && class_exists('Illuminate\Cache\Repository') && class_exists('Illuminate\Cache\ArrayStore')) {
33
+ $app->singleton('cache.store', fn () => new Illuminate\Cache\Repository(new Illuminate\Cache\ArrayStore()));
34
+ $app->singleton('cache', fn ($app) => $app->make('cache.store'));
35
+ }
36
+
37
+ if (!$app->bound('session.store') && class_exists('Illuminate\Session\Store') && class_exists('Illuminate\Session\ArraySessionHandler')) {
38
+ $app->singleton('session.store', fn () => new Illuminate\Session\Store('pest', new Illuminate\Session\ArraySessionHandler(120)));
39
+ $app->singleton('session', fn ($app) => $app->make('session.store'));
40
+ }
41
+
42
+ if (!$app->bound('log') && class_exists('Psr\Log\NullLogger')) {
43
+ if (!class_exists('Fleetbase\TestSupport\LoggerManager')) {
44
+ eval('namespace Fleetbase\TestSupport; class LoggerManager extends \Psr\Log\NullLogger { public function channel(?string $name = null): self { return $this; } }');
45
+ }
46
+
47
+ $app->singleton('log', fn () => new Fleetbase\TestSupport\LoggerManager());
48
+ }
49
+ }
50
+
51
+ if (!function_exists('app')) {
52
+ function app(?string $abstract = null, array $parameters = []): mixed
53
+ {
54
+ if (class_exists('Illuminate\Container\Container')) {
55
+ $container = Illuminate\Container\Container::getInstance();
56
+
57
+ return $abstract === null ? $container : $container->make($abstract, $parameters);
58
+ }
59
+
60
+ return $abstract === null ? null : new $abstract(...array_values($parameters));
61
+ }
62
+ }
63
+
64
+ if (!function_exists('request')) {
65
+ function request(?string $key = null, mixed $default = null): mixed
66
+ {
67
+ $request = class_exists('Illuminate\Http\Request') ? Illuminate\Http\Request::create('/') : new stdClass();
68
+
69
+ return $key === null ? $request : $default;
70
+ }
71
+ }
72
+
73
+ if (!function_exists('response')) {
74
+ function response(): object
75
+ {
76
+ return new class {
77
+ public function json(mixed $data = [], int $status = 200): mixed
78
+ {
79
+ if (class_exists('Illuminate\Http\JsonResponse')) {
80
+ return new Illuminate\Http\JsonResponse($data, $status);
81
+ }
82
+
83
+ return new class($data, $status) {
84
+ public function __construct(public mixed $data, public int $status) {}
85
+
86
+ public function getStatusCode(): int
87
+ {
88
+ return $this->status;
89
+ }
90
+ };
91
+ }
92
+
93
+ public function error(mixed $error = null, int $status = 500): mixed
94
+ {
95
+ return $this->json(['error' => $error], $status);
96
+ }
97
+ };
98
+ }
99
+ }
100
+
101
+ if (!function_exists('session')) {
102
+ function session(array|string|null $key = null, mixed $default = null): mixed
103
+ {
104
+ static $values = [];
105
+
106
+ if (is_array($key)) {
107
+ $values = array_merge($values, $key);
108
+
109
+ return null;
110
+ }
111
+
112
+ return $key === null ? $values : ($values[$key] ?? $default);
113
+ }
114
+ }
115
+
116
+ if (!function_exists('now') && class_exists('Illuminate\Support\Carbon')) {
117
+ function now($tz = null): Illuminate\Support\Carbon
118
+ {
119
+ return Illuminate\Support\Carbon::now($tz);
120
+ }
121
+ }
122
+
123
+ if (!trait_exists('Illuminate\Foundation\Auth\Access\AuthorizesRequests')) {
124
+ eval('namespace Illuminate\Foundation\Auth\Access; trait AuthorizesRequests {}');
125
+ }
126
+
127
+ if (!trait_exists('Illuminate\Foundation\Bus\Dispatchable')) {
128
+ eval('namespace Illuminate\Foundation\Bus; trait Dispatchable {}');
129
+ }
130
+
131
+ if (!trait_exists('Illuminate\Foundation\Bus\DispatchesJobs')) {
132
+ eval('namespace Illuminate\Foundation\Bus; trait DispatchesJobs {}');
133
+ }
134
+
135
+ if (!trait_exists('Illuminate\Foundation\Validation\ValidatesRequests')) {
136
+ eval('namespace Illuminate\Foundation\Validation; trait ValidatesRequests {}');
137
+ }
138
+
139
+ if (!class_exists('Illuminate\Foundation\Http\FormRequest') && class_exists('Illuminate\Http\Request')) {
140
+ eval('namespace Illuminate\Foundation\Http; class FormRequest extends \Illuminate\Http\Request { public function authorize(): bool { return true; } public function rules(): array { return []; } public function responseWithErrors(\Illuminate\Contracts\Validation\Validator $validator) { return $validator; } }');
141
+ }
142
+
143
+ if (!interface_exists('Fleetbase\Ai\Contracts\AIContextCapabilityInterface')) {
144
+ eval('namespace Fleetbase\Ai\Contracts; interface AIContextCapabilityInterface {}');
145
+ }
146
+
147
+ if (!interface_exists('Fleetbase\Ai\Contracts\AIActionCapabilityInterface')) {
148
+ eval('namespace Fleetbase\Ai\Contracts; interface AIActionCapabilityInterface {}');
149
+ }
150
+
151
+ if (!class_exists('Fleetbase\Ai\Models\AiTask')) {
152
+ eval('namespace Fleetbase\Ai\Models; class AiTask { public function __construct(array $attributes = []) { foreach ($attributes as $key => $value) { $this->{$key} = $value; } } }');
153
+ }
154
+
155
+ if (!class_exists('Fleetbase\Ai\Support\Capabilities\AbstractAICapability')) {
156
+ eval('namespace Fleetbase\Ai\Support\Capabilities; abstract class AbstractAICapability {}');
157
+ }
158
+
159
+ if (!class_exists('Fleetbase\Ai\Support\AiQueryableResource')) {
160
+ eval('namespace Fleetbase\Ai\Support; class AiQueryableResource { public string $key; public array $fields; public array $aliases; public function __construct(string $key, string $label = "", string $module = "", string $modelClass = "", string $permission = "", array $aliases = [], array $fields = [], array $sampleFields = [], ?string $locationField = null, ?string $directivePermission = null, int $maxLimit = 100) { $this->key = $key; $this->fields = $fields; $this->aliases = $aliases; } public function hasField(string $field): bool { return array_key_exists($field, $this->fields); } }');
161
+ }
162
+
163
+ if (!class_exists('Fleetbase\Ai\Support\AiQueryRegistry')) {
164
+ eval('namespace Fleetbase\Ai\Support; class AiQueryRegistry { private array $resources = []; public function register(AiQueryableResource $resource): void { $this->resources[$resource->key] = $resource; foreach ($resource->aliases as $alias) { $this->resources[$alias] = $resource; } } public function find(string $key): ?AiQueryableResource { return $this->resources[$key] ?? null; } }');
165
+ }
166
+
167
+ if (!class_exists('Fleetbase\Ai\Support\AiRelativeDateResolver') && class_exists('Illuminate\Support\Carbon')) {
168
+ eval('namespace Fleetbase\Ai\Support; class AiRelativeDateResolver { public function __construct($parser = null) {} public function resolveDateTime(string $prompt, ?string $timezone = null): ?\Illuminate\Support\Carbon { if (preg_match("/(\d+)\s+days?\s+from\s+now/i", $prompt, $matches)) { return \Illuminate\Support\Carbon::now($timezone)->addDays((int) $matches[1]); } return null; } public function resolveWindow(string $prompt, ?string $timezone = null): ?array { $timezone = $timezone ?: date_default_timezone_get(); $now = \Illuminate\Support\Carbon::now($timezone); if (str_contains(strtolower($prompt), "last week")) { $start = $now->copy()->subWeek()->startOfWeek(); $end = $now->copy()->subWeek()->endOfWeek(); return ["label" => "last week", "timezone" => $timezone, "start" => $start, "end" => $end]; } if (str_contains(strtolower($prompt), "yesterday")) { $start = $now->copy()->subDay()->startOfDay(); $end = $now->copy()->subDay()->endOfDay(); return ["label" => "yesterday", "timezone" => $timezone, "start" => $start, "end" => $end]; } return null; } }');
169
+ }
170
+
171
+ set_error_handler(function (int $severity, string $message): bool {
172
+ if (str_contains($message, '/pestphp/pest/vendor/autoload.php')) {
173
+ return true;
174
+ }
175
+
176
+ return false;
177
+ }, E_WARNING);
@@ -0,0 +1,82 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ $autoloadCandidates = [
6
+ getcwd() . '/server_vendor/autoload.php',
7
+ getcwd() . '/vendor/autoload.php',
8
+ ];
9
+
10
+ $autoload = null;
11
+ foreach ($autoloadCandidates as $candidate) {
12
+ if (is_file($candidate)) {
13
+ $autoload = $candidate;
14
+ break;
15
+ }
16
+ }
17
+
18
+ if ($autoload === null) {
19
+ fwrite(STDERR, "Unable to find Composer autoload. Run composer install first.\n");
20
+ exit(1);
21
+ }
22
+
23
+ require_once $autoload;
24
+
25
+ $runner = getcwd() . '/scripts/pest-runner.php';
26
+ if (!is_file($runner)) {
27
+ fwrite(STDERR, "Unable to find Pest runner at scripts/pest-runner.php.\n");
28
+ exit(1);
29
+ }
30
+
31
+ $testsPath = getcwd() . '/server/tests';
32
+ $files = glob($testsPath . '/*.php') ?: [];
33
+ sort($files);
34
+
35
+ if ($files === []) {
36
+ fwrite(STDERR, "Unable to find Pest test files in server/tests.\n");
37
+ exit(1);
38
+ }
39
+
40
+ $args = array_slice($argv, 1);
41
+ $timeout = (float) (getenv('PEST_FILE_TIMEOUT') ?: 120);
42
+
43
+ foreach ($files as $file) {
44
+ $relativeFile = str_replace(getcwd() . '/', '', $file);
45
+
46
+ fwrite(STDOUT, "::group::{$relativeFile}\n");
47
+
48
+ $command = array_merge([PHP_BINARY, $runner], $args, [$file]);
49
+ $timeoutBinary = trim((string) shell_exec('command -v timeout'));
50
+ if ($timeoutBinary !== '') {
51
+ $command = array_merge([$timeoutBinary, "{$timeout}s"], $command);
52
+ }
53
+
54
+ fwrite(STDOUT, '$ ' . implode(' ', array_map('escapeshellarg', $command)) . "\n");
55
+
56
+ passthru(implode(' ', array_map('escapeshellarg', $command)), $exitCode);
57
+
58
+ if ($exitCode === 124) {
59
+ fwrite(STDERR, "\nTimed out after {$timeout} seconds while running {$relativeFile}.\n");
60
+ fwrite(STDOUT, "::endgroup::\n");
61
+ exit(1);
62
+ }
63
+
64
+ fwrite(STDOUT, "::endgroup::\n");
65
+
66
+ if ($exitCode !== 0) {
67
+ if (!in_array('--debug', $args, true)) {
68
+ fwrite(STDOUT, "::group::{$relativeFile} debug\n");
69
+ $debugCommand = array_merge([PHP_BINARY, $runner], $args, ['--debug', $file]);
70
+ if ($timeoutBinary !== '') {
71
+ $debugCommand = array_merge([$timeoutBinary, "{$timeout}s"], $debugCommand);
72
+ }
73
+
74
+ fwrite(STDOUT, '$ ' . implode(' ', array_map('escapeshellarg', $debugCommand)) . "\n");
75
+ passthru(implode(' ', array_map('escapeshellarg', $debugCommand)));
76
+ fwrite(STDOUT, "::endgroup::\n");
77
+ }
78
+
79
+ fwrite(STDERR, "\nPest failed for {$relativeFile} with exit code {$exitCode}.\n");
80
+ exit($exitCode);
81
+ }
82
+ }
@@ -0,0 +1,64 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ $pestCandidates = [
6
+ getcwd() . '/server_vendor/bin/pest',
7
+ getcwd() . '/vendor/bin/pest',
8
+ getcwd() . '/server_vendor/pestphp/pest/bin/pest',
9
+ getcwd() . '/vendor/pestphp/pest/bin/pest',
10
+ ];
11
+
12
+ $pest = null;
13
+ foreach ($pestCandidates as $candidate) {
14
+ if (is_file($candidate)) {
15
+ $pest = $candidate;
16
+ break;
17
+ }
18
+ }
19
+
20
+ if ($pest === null) {
21
+ fwrite(STDERR, "Unable to find Pest. Run composer install first.\n");
22
+ exit(1);
23
+ }
24
+
25
+ $serverVendor = getcwd() . '/server_vendor';
26
+ $vendor = getcwd() . '/vendor';
27
+ if (!file_exists($vendor) && is_dir($serverVendor) && function_exists('symlink')) {
28
+ @symlink($serverVendor, $vendor);
29
+ }
30
+
31
+ $bootstrap = getcwd() . '/scripts/pest-bootstrap.php';
32
+ if (!is_file($bootstrap)) {
33
+ fwrite(STDERR, "Unable to find Pest bootstrap at scripts/pest-bootstrap.php.\n");
34
+ exit(1);
35
+ }
36
+
37
+ $args = array_slice($argv, 1);
38
+ $hasConfiguration = false;
39
+ foreach ($args as $arg) {
40
+ if (str_starts_with($arg, '--configuration')) {
41
+ $hasConfiguration = true;
42
+ break;
43
+ }
44
+ }
45
+ $configuration = getcwd() . '/phpunit.xml.dist';
46
+
47
+ if (!$hasConfiguration && is_file($configuration)) {
48
+ array_unshift($args, '--configuration=' . $configuration);
49
+ }
50
+
51
+ $command = array_merge([
52
+ PHP_BINARY,
53
+ '-d',
54
+ 'display_errors=1',
55
+ '-d',
56
+ 'error_reporting=8191',
57
+ '-d',
58
+ 'auto_prepend_file=' . $bootstrap,
59
+ $pest,
60
+ ], $args);
61
+
62
+ passthru(implode(' ', array_map('escapeshellarg', $command)), $exitCode);
63
+
64
+ exit($exitCode);
@@ -24,8 +24,14 @@ class Fleet extends FleetbaseResource
24
24
 
25
25
  $this->load($with);
26
26
 
27
- if (in_array('subfleets', $with, true) && in_array('vehicles', $with, true)) {
28
- $this->loadMissing('subFleets.vehicles');
27
+ if (in_array('subfleets', $with, true)) {
28
+ if (in_array('drivers', $with, true)) {
29
+ $this->loadMissing('subFleets.drivers');
30
+ }
31
+
32
+ if (in_array('vehicles', $with, true)) {
33
+ $this->loadMissing('subFleets.vehicles');
34
+ }
29
35
  }
30
36
  }
31
37
 
@@ -278,6 +278,8 @@ class Driver extends Model
278
278
  'photo_uuid',
279
279
  'avatar_url',
280
280
  'public_id',
281
+ 'name',
282
+ 'internal_id',
281
283
  'location',
282
284
  'online',
283
285
  'updated_at',
@@ -288,6 +290,10 @@ class Driver extends Model
288
290
  'year',
289
291
  'make',
290
292
  'model',
293
+ 'class',
294
+ 'color',
295
+ 'call_sign',
296
+ 'status',
291
297
  'specs',
292
298
  'vin_data',
293
299
  'telematics',
@@ -849,7 +849,7 @@ class Order extends Model
849
849
  $attributes['type'] = $this->type;
850
850
  }
851
851
 
852
- if (isset($attributes['pickup']) && is_array($attributes['pickup'])) {
852
+ if ($this->shouldResolvePayloadPlace($attributes, 'pickup')) {
853
853
  $pickup = Place::createFromMixed($attributes['pickup']);
854
854
 
855
855
  if ($pickup instanceof Place) {
@@ -857,7 +857,7 @@ class Order extends Model
857
857
  }
858
858
  }
859
859
 
860
- if (isset($attributes['dropoff']) && is_array($attributes['dropoff'])) {
860
+ if ($this->shouldResolvePayloadPlace($attributes, 'dropoff')) {
861
861
  $dropoff = Place::createFromMixed($attributes['dropoff']);
862
862
 
863
863
  if ($dropoff instanceof Place) {
@@ -865,7 +865,7 @@ class Order extends Model
865
865
  }
866
866
  }
867
867
 
868
- if (isset($attributes['return']) && is_array($attributes['return'])) {
868
+ if ($this->shouldResolvePayloadPlace($attributes, 'return')) {
869
869
  $return = Place::createFromMixed($attributes['return']);
870
870
 
871
871
  if ($return instanceof Place) {
@@ -897,19 +897,19 @@ class Order extends Model
897
897
  $attributes['type'] = $this->type;
898
898
  }
899
899
 
900
- if (isset($attributes['pickup']) && is_array($attributes['pickup'])) {
900
+ if ($this->shouldResolvePayloadPlace($attributes, 'pickup')) {
901
901
  $pickupId = Place::insertFromMixed($attributes['pickup']);
902
902
 
903
903
  $attributes['pickup_uuid'] = $pickupId;
904
904
  }
905
905
 
906
- if (isset($attributes['dropoff']) && is_array($attributes['dropoff'])) {
906
+ if ($this->shouldResolvePayloadPlace($attributes, 'dropoff')) {
907
907
  $dropoffId = Place::insertFromMixed($attributes['dropoff']);
908
908
 
909
909
  $attributes['dropoff_uuid'] = $dropoffId;
910
910
  }
911
911
 
912
- if (isset($attributes['return']) && is_array($attributes['return'])) {
912
+ if ($this->shouldResolvePayloadPlace($attributes, 'return')) {
913
913
  $returnId = Place::insertFromMixed($attributes['return']);
914
914
 
915
915
  $attributes['return_uuid'] = $returnId;
@@ -947,6 +947,18 @@ class Order extends Model
947
947
  return $payload;
948
948
  }
949
949
 
950
+ protected function shouldResolvePayloadPlace(?array $attributes, string $role): bool
951
+ {
952
+ return !$this->hasExistingPayloadPlaceUuid($attributes, $role) && isset($attributes[$role]) && is_array($attributes[$role]);
953
+ }
954
+
955
+ protected function hasExistingPayloadPlaceUuid(?array $attributes, string $role): bool
956
+ {
957
+ $uuid = data_get($attributes, "{$role}_uuid");
958
+
959
+ return is_string($uuid) && Str::isUuid($uuid) && Place::where('uuid', $uuid)->exists();
960
+ }
961
+
950
962
  /**
951
963
  * Retrieves the payload associated with the order.
952
964
  * If the payload is not already loaded, it loads the payload relationship.