@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.
- package/addon/components/ai/create-order-preview.hbs +297 -0
- package/addon/components/ai/create-order-preview.js +362 -0
- package/addon/components/ai/route-preview-map.hbs +79 -0
- package/addon/components/ai/route-preview-map.js +527 -0
- package/addon/components/fleet/details.hbs +21 -8
- package/addon/components/fleet/details.js +55 -1
- package/addon/components/fleet/hierarchy-tree.hbs +137 -0
- package/addon/components/fleet/hierarchy-tree.js +528 -0
- package/addon/components/map/drawer/device-event-listing.js +1 -0
- package/addon/components/map/drawer/driver-listing.js +1 -0
- package/addon/components/map/drawer/place-listing.js +1 -0
- package/addon/components/map/drawer/vehicle-listing.js +1 -0
- package/addon/components/order/details.hbs +34 -1
- package/addon/components/order/form/details.hbs +4 -0
- package/addon/components/order/form.hbs +9 -1
- package/addon/components/vehicle/card.hbs +14 -2
- package/addon/components/vehicle/form.hbs +13 -1
- package/addon/controllers/settings/map.js +4 -0
- package/addon/extension.js +14 -0
- package/addon/routes/management/fleets/index/details.js +1 -1
- package/addon/services/map-adapter/google.js +56 -16
- package/addon/services/map-manager.js +12 -0
- package/addon/styles/fleetops-engine.css +487 -1
- package/addon/utils/map-drawer-dropdown-position.js +11 -13
- package/app/components/ai/create-order-preview.js +1 -0
- package/app/components/ai/route-preview-map.js +1 -0
- package/app/components/fleet/hierarchy-tree.js +1 -0
- package/app/services/route-engine.js +1 -0
- package/composer.json +9 -2
- package/extension.json +1 -1
- package/package.json +6 -4
- package/phpunit.xml.dist +4 -4
- package/pnpm-workspace.yaml +2 -1
- package/scripts/coverage-runner.php +51 -0
- package/scripts/coverage-summary.php +110 -0
- package/scripts/normalize-rgba-alpha.js +90 -0
- package/scripts/pest-bootstrap.php +177 -0
- package/scripts/pest-file-runner.php +82 -0
- package/scripts/pest-runner.php +64 -0
- package/server/src/Http/Resources/v1/Fleet.php +8 -2
- package/server/src/Models/Driver.php +6 -0
- package/server/src/Models/Order.php +18 -6
- package/server/src/Models/Place.php +11 -3
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +3 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +26 -0
- package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +88 -0
- package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +108 -0
- package/server/src/Support/Ai/Capabilities/ConsoleNavigationCapability.php +55 -0
- package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +508 -0
- package/server/src/Support/Ai/Capabilities/DocsHelpCapability.php +49 -0
- package/server/src/Support/Ai/Capabilities/ImportOrdersPreviewCapability.php +66 -0
- package/server/src/Support/Ai/Capabilities/OperationalQueryCapability.php +319 -0
- package/server/src/Support/Ai/Capabilities/OptimizeOrderRouteCapability.php +240 -0
- package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +95 -0
- package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +195 -0
- package/server/src/Support/Ai/FleetOpsAiQueryResources.php +150 -0
- package/server/src/Support/Utils.php +52 -0
- package/server/src/Tracking/TrackingOptions.php +6 -1
- package/server/src/routes.php +0 -11
- package/server/tests/AiCreateOrderPreviewCapabilityTest.php +131 -0
- package/server/tests/AiOperationalQueryCapabilityTest.php +75 -0
- package/server/tests/AnalyticsRoutesTest.php +5 -3
- package/server/tests/CustomerEndpointTest.php +1 -0
- package/server/tests/DeviceFilterTest.php +1 -2
- package/server/tests/FleetHierarchyResourceTest.php +12 -0
- package/server/tests/FuelProviderRegistryTest.php +7 -9
- package/server/tests/LiveControllerViewportTest.php +8 -16
- package/server/tests/LiveOrderQueryTest.php +18 -24
- package/server/tests/OrderPayloadPlaceResolutionTest.php +53 -0
- package/server/tests/PointResolutionTest.php +63 -0
- package/server/tests/ServiceAreaZoneApiShapeTest.php +8 -9
- package/server/tests/ServiceRateAlgorithmWeightVariablesTest.php +1 -1
- package/server/tests/TelematicsHardeningTest.php +31 -26
- package/server/tests/TrackingIntelligenceTest.php +65 -25
- package/server/tests/VehicleDriverAssignmentTest.php +8 -9
- package/addon/components/table/cell/dropdown/action-item.hbs +0 -31
- package/addon/components/table/cell/dropdown/action-item.js +0 -62
- package/addon/components/table/cell/dropdown.hbs +0 -28
- package/addon/components/table/cell/dropdown.js +0 -91
- package/app/components/table/cell/dropdown/action-item.js +0 -1
- package/app/components/table/cell/dropdown.js +0 -1
- 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)
|
|
28
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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.
|