@fleetbase/fleetops-engine 0.6.8 → 0.6.9
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/activity-event-selector.js +4 -0
- package/addon/components/display-place.hbs +27 -3
- package/addon/components/route-list.hbs +3 -3
- package/addon/controllers/operations/orders/index.js +0 -4
- package/addon/routes/operations/orders/index/view.js +18 -1
- package/addon/services/movement-tracker.js +1 -1
- package/addon/styles/fleetops-engine.css +7 -0
- package/addon/templates/operations/orders/index/new.hbs +2 -2
- package/addon/templates/operations/orders/index/view.hbs +5 -1
- package/addon/templates/operations/orders/index.hbs +6 -1
- package/addon/templates/settings/notifications.hbs +9 -1
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +1 -1
- package/server/src/Console/Commands/TrackOrderDistanceAndTime.php +2 -2
- package/server/src/Events/OrderCanceled.php +6 -0
- package/server/src/Events/OrderCompleted.php +6 -0
- package/server/src/Events/OrderDispatched.php +6 -0
- package/server/src/Events/OrderFailed.php +6 -0
- package/server/src/Events/WaypointActivityChanged.php +119 -0
- package/server/src/Events/WaypointCompleted.php +119 -0
- package/server/src/Flow/Activity.php +28 -10
- package/server/src/Flow/Event.php +26 -2
- package/server/src/Http/Controllers/Api/v1/DriverController.php +19 -2
- package/server/src/Http/Controllers/Api/v1/OrderController.php +274 -164
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +15 -8
- package/server/src/Http/Filter/OrderFilter.php +4 -4
- package/server/src/Http/Resources/v1/Payload.php +1 -1
- package/server/src/Listeners/HandleOrderCanceled.php +0 -10
- package/server/src/Listeners/NotifyOrderEvent.php +4 -4
- package/server/src/Models/OrderConfig.php +50 -35
- package/server/src/Models/Payload.php +26 -7
- package/server/src/Models/Waypoint.php +10 -0
- package/server/src/Notifications/OrderAssigned.php +3 -5
- package/server/src/Notifications/OrderCanceled.php +32 -12
- package/server/src/Notifications/OrderCompleted.php +31 -11
- package/server/src/Notifications/OrderDispatchFailed.php +3 -5
- package/server/src/Notifications/OrderDispatched.php +31 -11
- package/server/src/Notifications/OrderFailed.php +32 -12
- package/server/src/Notifications/OrderPing.php +2 -6
- package/server/src/Notifications/OrderSplit.php +1 -1
- package/server/src/Notifications/WaypointCompleted.php +157 -0
|
@@ -32,10 +32,12 @@ use Fleetbase\Models\Setting;
|
|
|
32
32
|
use Fleetbase\Support\Auth;
|
|
33
33
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
34
34
|
use Illuminate\Http\Request;
|
|
35
|
+
use Illuminate\Http\UploadedFile;
|
|
35
36
|
use Illuminate\Support\Arr;
|
|
36
37
|
use Illuminate\Support\Carbon;
|
|
37
38
|
use Illuminate\Support\Facades\Storage;
|
|
38
39
|
use Illuminate\Support\Str;
|
|
40
|
+
use Illuminate\Validation\ValidationException;
|
|
39
41
|
|
|
40
42
|
class OrderController extends Controller
|
|
41
43
|
{
|
|
@@ -295,15 +297,6 @@ class OrderController extends Controller
|
|
|
295
297
|
// create the order
|
|
296
298
|
$order = Order::create($input);
|
|
297
299
|
|
|
298
|
-
// notify driver if assigned
|
|
299
|
-
$order->notifyDriverAssigned();
|
|
300
|
-
|
|
301
|
-
// set driving distance and time
|
|
302
|
-
$order->setPreliminaryDistanceAndTime();
|
|
303
|
-
|
|
304
|
-
// if service quote attached purchase
|
|
305
|
-
$order->purchaseServiceQuote($serviceQuote);
|
|
306
|
-
|
|
307
300
|
// if it's integrated vendor order apply to meta
|
|
308
301
|
if ($integratedVendorOrder) {
|
|
309
302
|
$order->updateMeta([
|
|
@@ -312,16 +305,32 @@ class OrderController extends Controller
|
|
|
312
305
|
]);
|
|
313
306
|
}
|
|
314
307
|
|
|
315
|
-
// dispatch if flagged true
|
|
316
|
-
if ($request->boolean('dispatch') && $integratedVendorOrder === null) {
|
|
317
|
-
$order->dispatchWithActivity();
|
|
318
|
-
}
|
|
319
|
-
|
|
320
308
|
// load required relations
|
|
321
309
|
$order->load(['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']);
|
|
322
310
|
|
|
323
|
-
//
|
|
324
|
-
|
|
311
|
+
// Determine if order should be dispatched on creation
|
|
312
|
+
$shouldDispatch = $request->boolean('dispatch') && $integratedVendorOrder === null;
|
|
313
|
+
|
|
314
|
+
// Run background processes on queue
|
|
315
|
+
dispatch(function () use ($order, $serviceQuote, $shouldDispatch): void {
|
|
316
|
+
// notify driver if assigned
|
|
317
|
+
$order->notifyDriverAssigned();
|
|
318
|
+
|
|
319
|
+
// set driving distance and time
|
|
320
|
+
$order->setPreliminaryDistanceAndTime();
|
|
321
|
+
|
|
322
|
+
// if service quote attached purchase
|
|
323
|
+
$order->purchaseServiceQuote($serviceQuote);
|
|
324
|
+
|
|
325
|
+
// dispatch if flagged true
|
|
326
|
+
if ($shouldDispatch) {
|
|
327
|
+
$order->dispatchWithActivity();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Trigger order created event
|
|
331
|
+
event(new OrderReady($order));
|
|
332
|
+
})
|
|
333
|
+
->afterCommit();
|
|
325
334
|
|
|
326
335
|
// response the driver resource
|
|
327
336
|
return new OrderResource($order);
|
|
@@ -934,18 +943,13 @@ class OrderController extends Controller
|
|
|
934
943
|
try {
|
|
935
944
|
$order = Order::findRecordOrFail($id, ['driverAssigned', 'payload.entities', 'payload.currentWaypoint', 'payload.waypoints']);
|
|
936
945
|
} catch (ModelNotFoundException $exception) {
|
|
937
|
-
return response()->
|
|
938
|
-
[
|
|
939
|
-
'error' => 'Order resource not found.',
|
|
940
|
-
],
|
|
941
|
-
404
|
|
942
|
-
);
|
|
946
|
+
return response()->apiError('Order resource not found.', 404);
|
|
943
947
|
}
|
|
944
948
|
}
|
|
945
949
|
|
|
946
950
|
// if no order found
|
|
947
951
|
if (!$order) {
|
|
948
|
-
return response()->apiError('
|
|
952
|
+
return response()->apiError('Order resource not found.', 404);
|
|
949
953
|
}
|
|
950
954
|
|
|
951
955
|
// if order is still status of `created` trigger started flag
|
|
@@ -988,13 +992,17 @@ class OrderController extends Controller
|
|
|
988
992
|
/** @var \Fleetbase\LaravelMysqlSpatial\Types\Point */
|
|
989
993
|
$location = $order->getLastLocation();
|
|
990
994
|
|
|
995
|
+
// Check if multiple waypoint order to update activity for
|
|
996
|
+
$isMultipleWaypointOrder = (bool) $order->payload->isMultipleDropOrder;
|
|
997
|
+
|
|
991
998
|
// if is multi drop order and no current destination set it
|
|
992
|
-
if ($
|
|
999
|
+
if ($isMultipleWaypointOrder && !$order->payload->current_waypoint_uuid) {
|
|
993
1000
|
$order->payload->setFirstWaypoint($activity, $location);
|
|
994
1001
|
}
|
|
995
1002
|
|
|
996
|
-
|
|
997
|
-
|
|
1003
|
+
// Handle multiple dropoff waypoint activity
|
|
1004
|
+
if (Utils::isActivity($activity) && $activity->completesOrder() && $isMultipleWaypointOrder) {
|
|
1005
|
+
// Check if every waypoint is completed
|
|
998
1006
|
$isCompleted = $order->payload->waypointMarkers->every(function ($waypoint) {
|
|
999
1007
|
return $waypoint->complete;
|
|
1000
1008
|
});
|
|
@@ -1016,17 +1024,22 @@ class OrderController extends Controller
|
|
|
1016
1024
|
}
|
|
1017
1025
|
}
|
|
1018
1026
|
|
|
1019
|
-
// Update activity
|
|
1020
|
-
$order->updateActivity($activity, $proof);
|
|
1021
|
-
|
|
1022
1027
|
// also update for each order entities if not multiple drop order
|
|
1023
1028
|
// all entities will share the same activity status as is one drop order
|
|
1024
|
-
if (!$
|
|
1029
|
+
if (!$isMultipleWaypointOrder) {
|
|
1030
|
+
// Update order activity
|
|
1031
|
+
$order->updateActivity($activity, $proof);
|
|
1032
|
+
|
|
1025
1033
|
// Only update entities belonging to the waypoint
|
|
1026
1034
|
foreach ($order->payload->entities as $entity) {
|
|
1027
1035
|
$entity->insertActivity($activity, $location, $proof);
|
|
1028
1036
|
}
|
|
1029
1037
|
} else {
|
|
1038
|
+
// Update parent order when status is `dispatched` or `started`
|
|
1039
|
+
if (in_array($activity->code, ['started', 'dispatched'])) {
|
|
1040
|
+
$order->updateActivity($activity, $proof);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1030
1043
|
$order->payload->updateWaypointActivity($activity, $location);
|
|
1031
1044
|
}
|
|
1032
1045
|
|
|
@@ -1045,8 +1058,10 @@ class OrderController extends Controller
|
|
|
1045
1058
|
*
|
|
1046
1059
|
* @return \Illuminate\Http\Response
|
|
1047
1060
|
*/
|
|
1048
|
-
public function getNextActivity(string $id)
|
|
1061
|
+
public function getNextActivity(string $id, Request $request)
|
|
1049
1062
|
{
|
|
1063
|
+
$waypointId = $request->input('waypoint');
|
|
1064
|
+
|
|
1050
1065
|
try {
|
|
1051
1066
|
$order = Order::findRecordOrFail($id, ['payload']);
|
|
1052
1067
|
} catch (ModelNotFoundException $exception) {
|
|
@@ -1058,7 +1073,15 @@ class OrderController extends Controller
|
|
|
1058
1073
|
);
|
|
1059
1074
|
}
|
|
1060
1075
|
|
|
1061
|
-
|
|
1076
|
+
// Get waypoint record if available
|
|
1077
|
+
$waypoint = null;
|
|
1078
|
+
if ($waypointId) {
|
|
1079
|
+
$waypoint = Waypoint::where('payload_uuid', $order->payload_uuid)->whereHas('place', function ($query) use ($waypointId) {
|
|
1080
|
+
$query->where('public_id', $waypointId);
|
|
1081
|
+
})->first();
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
$activities = $order->config()->nextActivity($waypoint);
|
|
1062
1085
|
|
|
1063
1086
|
// If activity is to complete order add proof of delivery properties if required
|
|
1064
1087
|
// This is a temporary fix until activity is updated to handle POD on it's own
|
|
@@ -1149,11 +1172,14 @@ class OrderController extends Controller
|
|
|
1149
1172
|
public function setDestination(string $id, string $placeId)
|
|
1150
1173
|
{
|
|
1151
1174
|
try {
|
|
1152
|
-
$order = Order::
|
|
1175
|
+
$order = Order::findRecordOrFail($id);
|
|
1153
1176
|
} catch (ModelNotFoundException $exception) {
|
|
1154
1177
|
return response()->apiError('Order resource not found.', 404);
|
|
1155
1178
|
}
|
|
1156
1179
|
|
|
1180
|
+
// Load required relations
|
|
1181
|
+
$order->loadMissing(['payload.waypoints', 'payload.pickup', 'payload.dropoff']);
|
|
1182
|
+
|
|
1157
1183
|
// Get the order payload
|
|
1158
1184
|
$payload = $order->payload;
|
|
1159
1185
|
|
|
@@ -1246,39 +1272,19 @@ class OrderController extends Controller
|
|
|
1246
1272
|
$rawData = $request->input('raw_data');
|
|
1247
1273
|
$type = $subjectId ? strtok($subjectId, '_') : null;
|
|
1248
1274
|
|
|
1249
|
-
try {
|
|
1250
|
-
$order = Order::findRecordOrFail($id);
|
|
1251
|
-
} catch (ModelNotFoundException $e) {
|
|
1252
|
-
return response()->apiError('Order resource not found.', 404);
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
1275
|
if (!$code) {
|
|
1256
1276
|
return response()->apiError('No QR code data to capture.');
|
|
1257
1277
|
}
|
|
1258
1278
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
$subject = Waypoint::where('payload_uuid', $order->payload_uuid)->where(function ($q) use ($code) {
|
|
1265
|
-
$q->whereHas('place', function ($q) use ($code) {
|
|
1266
|
-
$q->where('uuid', $code);
|
|
1267
|
-
});
|
|
1268
|
-
$q->orWhere('uuid', $code);
|
|
1269
|
-
})->withoutGlobalScopes()->first();
|
|
1270
|
-
break;
|
|
1271
|
-
|
|
1272
|
-
case 'entity':
|
|
1273
|
-
$subject = Entity::where('uuid', $code)->withoutGlobalScopes()->first();
|
|
1274
|
-
break;
|
|
1275
|
-
|
|
1276
|
-
case 'order':
|
|
1277
|
-
default:
|
|
1278
|
-
$subject = $order;
|
|
1279
|
-
break;
|
|
1279
|
+
// Load Order
|
|
1280
|
+
try {
|
|
1281
|
+
$order = Order::findRecordOrFail($id);
|
|
1282
|
+
} catch (ModelNotFoundException $e) {
|
|
1283
|
+
return response()->apiError('Order resource not found.', 404);
|
|
1280
1284
|
}
|
|
1281
1285
|
|
|
1286
|
+
// Resolve subject
|
|
1287
|
+
$subject = $this->resolveSubject($order, $type, $subjectId);
|
|
1282
1288
|
if (!$subject) {
|
|
1283
1289
|
return response()->apiError('Unable to capture QR code data.');
|
|
1284
1290
|
}
|
|
@@ -1316,39 +1322,19 @@ class OrderController extends Controller
|
|
|
1316
1322
|
$remarks = $request->input('remarks', 'Verified by Signature');
|
|
1317
1323
|
$type = $subjectId ? strtok($subjectId, '_') : null;
|
|
1318
1324
|
|
|
1319
|
-
try {
|
|
1320
|
-
$order = Order::findRecordOrFail($id);
|
|
1321
|
-
} catch (ModelNotFoundException $e) {
|
|
1322
|
-
return response()->apiError('Order resource not found.', 404);
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
1325
|
if (!$signature) {
|
|
1326
1326
|
return response()->apiError('No signature data to capture.');
|
|
1327
1327
|
}
|
|
1328
1328
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
$subject = Waypoint::where('payload_uuid', $order->payload_uuid)->where(function ($q) use ($subjectId) {
|
|
1335
|
-
$q->whereHas('place', function ($q) use ($subjectId) {
|
|
1336
|
-
$q->where('public_id', $subjectId);
|
|
1337
|
-
});
|
|
1338
|
-
$q->orWhere('public_id', $subjectId);
|
|
1339
|
-
})->withoutGlobalScopes()->first();
|
|
1340
|
-
break;
|
|
1341
|
-
|
|
1342
|
-
case 'entity':
|
|
1343
|
-
$subject = Entity::where('public_id', $subjectId)->withoutGlobalScopes()->first();
|
|
1344
|
-
break;
|
|
1345
|
-
|
|
1346
|
-
case 'order':
|
|
1347
|
-
default:
|
|
1348
|
-
$subject = $order;
|
|
1349
|
-
break;
|
|
1329
|
+
// Load Order
|
|
1330
|
+
try {
|
|
1331
|
+
$order = Order::findRecordOrFail($id);
|
|
1332
|
+
} catch (ModelNotFoundException $e) {
|
|
1333
|
+
return response()->apiError('Order resource not found.', 404);
|
|
1350
1334
|
}
|
|
1351
1335
|
|
|
1336
|
+
// Resolve subject
|
|
1337
|
+
$subject = $this->resolveSubject($order, $type, $subjectId);
|
|
1352
1338
|
if (!$subject) {
|
|
1353
1339
|
return response()->apiError('Unable to capture signature data.');
|
|
1354
1340
|
}
|
|
@@ -1392,94 +1378,220 @@ class OrderController extends Controller
|
|
|
1392
1378
|
}
|
|
1393
1379
|
|
|
1394
1380
|
/**
|
|
1395
|
-
*
|
|
1381
|
+
* Capture one or more photos for an order (as proof) and persist them.
|
|
1396
1382
|
*
|
|
1397
|
-
*
|
|
1383
|
+
* This endpoint supports **both**:
|
|
1384
|
+
* - `multipart/form-data` uploads (key: `photos[]`)
|
|
1385
|
+
* - JSON payload with Base64-encoded images (key: `photos`: [string…])
|
|
1386
|
+
*
|
|
1387
|
+
* It will:
|
|
1388
|
+
* 1. Validate that `photos` is a non-empty array of files or strings.
|
|
1389
|
+
* 2. Resolve the target Order and optional subject (waypoint/place/entity).
|
|
1390
|
+
* 3. Loop through each upload or blob, create a Proof record, decode/store the image,
|
|
1391
|
+
* then create a File record and link it to the Proof.
|
|
1392
|
+
*
|
|
1393
|
+
* @param string $id UUID or primary key of the Order
|
|
1394
|
+
* @param string|null $subjectId Optional “subject” identifier (e.g. waypoint_publicId)
|
|
1395
|
+
*
|
|
1396
|
+
* @return \Fleetbase\FleetOps\Http\Resources\ProofResource
|
|
1397
|
+
*
|
|
1398
|
+
* @throws ValidationException
|
|
1399
|
+
* @throws ModelNotFoundException
|
|
1398
1400
|
*/
|
|
1399
1401
|
public function capturePhoto(Request $request, string $id, ?string $subjectId = null)
|
|
1400
1402
|
{
|
|
1401
|
-
|
|
1402
|
-
$bucket = $request->input('bucket', config('filesystems.disks.' . $disk . '.bucket', config('filesystems.disks.s3.bucket')));
|
|
1403
|
-
$photo = $request->input('photo');
|
|
1404
|
-
$photos = $request->array('photos');
|
|
1405
|
-
$data = $request->input('data', []);
|
|
1406
|
-
$remarks = $request->input('remarks', 'Verified by Photo');
|
|
1407
|
-
$type = $subjectId ? strtok($subjectId, '_') : null;
|
|
1408
|
-
$photos = array_filter([$photo, ...$photos]);
|
|
1409
|
-
|
|
1403
|
+
// Validate incoming payload
|
|
1410
1404
|
try {
|
|
1411
|
-
$
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1405
|
+
$request->validate([
|
|
1406
|
+
'photos' => 'required|array|min:1',
|
|
1407
|
+
'photos.*' => [
|
|
1408
|
+
function ($attribute, $value, $fail) {
|
|
1409
|
+
// 1) If it’s a file, ensure it’s an image ≤ 10 MB
|
|
1410
|
+
if ($value instanceof UploadedFile) {
|
|
1411
|
+
if (!$value->isValid()
|
|
1412
|
+
|| !in_array($value->extension(), ['jpg', 'jpeg', 'png', 'gif'])
|
|
1413
|
+
|| $value->getSize() > 10 * 1024 * 1024
|
|
1414
|
+
) {
|
|
1415
|
+
$fail("{$attribute} must be a valid image file ≤ 10 MB.");
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1415
1420
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1421
|
+
// 2) Otherwise it must be a valid Base64 string
|
|
1422
|
+
if (is_string($value)) {
|
|
1423
|
+
// strict decode check
|
|
1424
|
+
if (base64_decode($value, true) === false) {
|
|
1425
|
+
$fail("{$attribute} is not a valid Base64 string.");
|
|
1426
|
+
}
|
|
1419
1427
|
|
|
1420
|
-
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1421
1430
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1431
|
+
// 3) Anything else is invalid
|
|
1432
|
+
$fail("{$attribute} must be an image file or a Base64 string.");
|
|
1433
|
+
},
|
|
1434
|
+
],
|
|
1435
|
+
'remarks' => 'sometimes|string|max:255',
|
|
1436
|
+
'data' => 'sometimes|array',
|
|
1437
|
+
]);
|
|
1438
|
+
} catch (ValidationException $e) {
|
|
1439
|
+
$errorMessage = collect($e->errors())->flatten()->first();
|
|
1440
|
+
|
|
1441
|
+
return response()->apiError($errorMessage, 422);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// Determine storage disk & bucket
|
|
1445
|
+
$disk = $request->input('disk', config('filesystems.default'));
|
|
1446
|
+
$bucket = $request->input(
|
|
1447
|
+
"filesystems.disks.{$disk}.bucket",
|
|
1448
|
+
config('filesystems.disks.s3.bucket')
|
|
1449
|
+
);
|
|
1450
|
+
|
|
1451
|
+
// Collect uploads & Base64 strings
|
|
1452
|
+
/** @var UploadedFile[] $rawInputs */
|
|
1453
|
+
$rawInputs = $request->file('photos', []);
|
|
1454
|
+
/** @var string[] $base64Inputs */
|
|
1455
|
+
$base64Inputs = array_filter(
|
|
1456
|
+
$request->input('photos', []),
|
|
1457
|
+
function ($value) {
|
|
1458
|
+
// must be a string AND strictly decodable as Base64
|
|
1459
|
+
return is_string($value) && base64_decode($value, true) !== false;
|
|
1460
|
+
}
|
|
1461
|
+
);
|
|
1462
|
+
|
|
1463
|
+
$remarks = $request->input('remarks', 'Verified by Photo');
|
|
1464
|
+
$data = $request->input('data', []);
|
|
1465
|
+
$type = $subjectId ? strtok($subjectId, '_') : null;
|
|
1466
|
+
|
|
1467
|
+
// Normalize into one array
|
|
1468
|
+
$incoming = array_merge($rawInputs, $base64Inputs);
|
|
1432
1469
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1470
|
+
if (empty($incoming)) {
|
|
1471
|
+
return response()->apiError('No photo data to capture.');
|
|
1472
|
+
}
|
|
1436
1473
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1474
|
+
// Load Order
|
|
1475
|
+
try {
|
|
1476
|
+
$order = Order::findRecordOrFail($id);
|
|
1477
|
+
} catch (ModelNotFoundException $e) {
|
|
1478
|
+
return response()->apiError('Order resource not found.', 404);
|
|
1441
1479
|
}
|
|
1442
1480
|
|
|
1481
|
+
// Resolve subject
|
|
1482
|
+
$subject = $this->resolveSubject($order, $type, $subjectId);
|
|
1443
1483
|
if (!$subject) {
|
|
1444
1484
|
return response()->apiError('Unable to capture photo as proof.');
|
|
1445
1485
|
}
|
|
1446
1486
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1487
|
+
// 5) Loop through each item, create Proof + File
|
|
1488
|
+
foreach ($incoming as $item) {
|
|
1449
1489
|
$proof = Proof::create([
|
|
1450
1490
|
'company_uuid' => session('company'),
|
|
1451
1491
|
'order_uuid' => $order->uuid,
|
|
1452
1492
|
'subject_uuid' => $subject->uuid,
|
|
1453
1493
|
'subject_type' => Utils::getModelClassName($subject),
|
|
1454
1494
|
'remarks' => $remarks,
|
|
1455
|
-
'raw_data' => $
|
|
1495
|
+
'raw_data' => $item instanceof UploadedFile ? null : $item,
|
|
1456
1496
|
'data' => $data,
|
|
1457
1497
|
]);
|
|
1458
1498
|
|
|
1459
|
-
$
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
'extension' => 'png',
|
|
1468
|
-
'content_type' => 'image/png',
|
|
1469
|
-
'path' => $path,
|
|
1470
|
-
'bucket' => $bucket,
|
|
1471
|
-
'type' => 'photo',
|
|
1472
|
-
'size' => Utils::getBase64ImageSize($photo),
|
|
1473
|
-
])->setKey($proof);
|
|
1474
|
-
|
|
1475
|
-
// set file to proof
|
|
1476
|
-
$proof->file_uuid = $file->uuid;
|
|
1477
|
-
$proof->save();
|
|
1499
|
+
$file = $this->storeProofPhoto(
|
|
1500
|
+
proof: $proof,
|
|
1501
|
+
photo: $item,
|
|
1502
|
+
disk: $disk,
|
|
1503
|
+
bucket: $bucket
|
|
1504
|
+
);
|
|
1505
|
+
|
|
1506
|
+
$proof->update(['file_uuid' => $file->uuid]);
|
|
1478
1507
|
}
|
|
1479
1508
|
|
|
1509
|
+
// Return the last Proof resource created
|
|
1480
1510
|
return new ProofResource($proof);
|
|
1481
1511
|
}
|
|
1482
1512
|
|
|
1513
|
+
/**
|
|
1514
|
+
* Decode and store a single proof image, then create its File record.
|
|
1515
|
+
*
|
|
1516
|
+
* @param UploadedFile|string $photo UploadedFile instance or Base64 string
|
|
1517
|
+
* @param string $disk Filesystem disk name
|
|
1518
|
+
* @param string $bucket Storage bucket/key prefix
|
|
1519
|
+
*
|
|
1520
|
+
* @return \Feetbase\Models\File
|
|
1521
|
+
*/
|
|
1522
|
+
protected function storeProofPhoto(
|
|
1523
|
+
Proof $proof,
|
|
1524
|
+
UploadedFile|string $photo,
|
|
1525
|
+
string $disk,
|
|
1526
|
+
string $bucket,
|
|
1527
|
+
): File {
|
|
1528
|
+
$isFile = $photo instanceof UploadedFile;
|
|
1529
|
+
$contents = $isFile
|
|
1530
|
+
? file_get_contents($photo->getRealPath())
|
|
1531
|
+
: base64_decode($photo);
|
|
1532
|
+
$extension = $isFile
|
|
1533
|
+
? $photo->getClientOriginalExtension()
|
|
1534
|
+
: 'png';
|
|
1535
|
+
$contentType = $isFile
|
|
1536
|
+
? $photo->getClientMimeType()
|
|
1537
|
+
: 'image/png';
|
|
1538
|
+
|
|
1539
|
+
$company = session('company');
|
|
1540
|
+
$path = "uploads/{$company}/photos/{$proof->public_id}.{$extension}";
|
|
1541
|
+
|
|
1542
|
+
Storage::disk($disk)->put($path, $contents);
|
|
1543
|
+
|
|
1544
|
+
return File::create([
|
|
1545
|
+
'company_uuid' => $company,
|
|
1546
|
+
'uploader_uuid' => session('user'),
|
|
1547
|
+
'name' => basename($path),
|
|
1548
|
+
'original_filename' => basename($path),
|
|
1549
|
+
'extension' => $extension,
|
|
1550
|
+
'content_type' => $contentType,
|
|
1551
|
+
'path' => $path,
|
|
1552
|
+
'bucket' => $bucket,
|
|
1553
|
+
'type' => 'photo',
|
|
1554
|
+
'size' => strlen($contents),
|
|
1555
|
+
])->setKey($proof);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* Resolve the “subject” model based on type and public ID.
|
|
1560
|
+
*
|
|
1561
|
+
* Supported types:
|
|
1562
|
+
* - null → the Order itself
|
|
1563
|
+
* - 'place', 'waypoint' → a Waypoint matching payload_uuid & public_id
|
|
1564
|
+
* - 'entity' → an Entity by public_id
|
|
1565
|
+
* - 'order' or any other → the Order
|
|
1566
|
+
*
|
|
1567
|
+
* @param string|null $type Type prefix extracted from subjectId
|
|
1568
|
+
* @param string|null $subjectId Full public_id of the subject
|
|
1569
|
+
*
|
|
1570
|
+
* @return Order|Waypoint|Entity|null
|
|
1571
|
+
*/
|
|
1572
|
+
protected function resolveSubject(Order $order, ?string $type, ?string $subjectId)
|
|
1573
|
+
{
|
|
1574
|
+
if (!$type) {
|
|
1575
|
+
return $order;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
return match ($type) {
|
|
1579
|
+
'place', 'waypoint' => Waypoint::withoutGlobalScopes()
|
|
1580
|
+
->where('payload_uuid', $order->payload_uuid)
|
|
1581
|
+
->where(fn ($q) => $q
|
|
1582
|
+
->whereHas('place', fn ($q) => $q->where('public_id', $subjectId))
|
|
1583
|
+
->orWhere('public_id', $subjectId)
|
|
1584
|
+
)
|
|
1585
|
+
->first(),
|
|
1586
|
+
|
|
1587
|
+
'entity' => Entity::withoutGlobalScopes()
|
|
1588
|
+
->where('public_id', $subjectId)
|
|
1589
|
+
->first(),
|
|
1590
|
+
|
|
1591
|
+
default => $order,
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1483
1595
|
/**
|
|
1484
1596
|
* Retrieve proof of delivery resources associated with a given order and optional subject.
|
|
1485
1597
|
*
|
|
@@ -1503,34 +1615,27 @@ class OrderController extends Controller
|
|
|
1503
1615
|
}
|
|
1504
1616
|
|
|
1505
1617
|
$subject = $order;
|
|
1506
|
-
|
|
1507
1618
|
if ($subjectId) {
|
|
1508
|
-
$type
|
|
1509
|
-
|
|
1510
|
-
$subject = match ($type) {
|
|
1511
|
-
'place', 'waypoint' => Waypoint::where('payload_uuid', $order->payload_uuid)
|
|
1512
|
-
->where(function ($query) use ($subjectId) {
|
|
1513
|
-
$query->whereHas('place', fn ($q) => $q->where('public_id', $subjectId))
|
|
1514
|
-
->orWhere('public_id', $subjectId);
|
|
1515
|
-
})
|
|
1516
|
-
->withoutGlobalScopes()
|
|
1517
|
-
->first(),
|
|
1518
|
-
|
|
1519
|
-
'entity' => Entity::where('public_id', $subjectId)->withoutGlobalScopes()->first(),
|
|
1520
|
-
|
|
1521
|
-
default => $order,
|
|
1522
|
-
};
|
|
1619
|
+
$type = strtok($subjectId, '_');
|
|
1620
|
+
$subject = $this->resolveSubject($order, $type, $subjectId);
|
|
1523
1621
|
}
|
|
1524
1622
|
|
|
1525
1623
|
if (!$subject) {
|
|
1526
1624
|
return response()->apiError('Unable to retrieve proof of delivery for subject.');
|
|
1527
1625
|
}
|
|
1528
1626
|
|
|
1529
|
-
$
|
|
1627
|
+
$proofsQuery = Proof::where([
|
|
1530
1628
|
'company_uuid' => session('company'),
|
|
1531
1629
|
'order_uuid' => $order->uuid,
|
|
1532
|
-
|
|
1533
|
-
|
|
1630
|
+
]);
|
|
1631
|
+
|
|
1632
|
+
// if subject is not the order then filter by subject
|
|
1633
|
+
if ($order->uuid !== $subject->uuid) {
|
|
1634
|
+
$proofsQuery->where('subject_uuid', $subject->uuid);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// get proofs
|
|
1638
|
+
$proofs = $proofsQuery->get();
|
|
1534
1639
|
|
|
1535
1640
|
return ProofResource::collection($proofs);
|
|
1536
1641
|
}
|
|
@@ -1574,6 +1679,11 @@ class OrderController extends Controller
|
|
|
1574
1679
|
return response()->json($entityEditingSettings);
|
|
1575
1680
|
}
|
|
1576
1681
|
|
|
1682
|
+
/**
|
|
1683
|
+
* Get all order comments.
|
|
1684
|
+
*
|
|
1685
|
+
* @return \Illuminate\Http\JsonResponse
|
|
1686
|
+
*/
|
|
1577
1687
|
public function orderComments(string $id)
|
|
1578
1688
|
{
|
|
1579
1689
|
try {
|
|
@@ -328,7 +328,7 @@ class OrderController extends FleetOpsController
|
|
|
328
328
|
return response()->error('Nothing to delete.');
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
/** @var
|
|
331
|
+
/** @var Order */
|
|
332
332
|
$count = Order::whereIn('uuid', $ids)->count();
|
|
333
333
|
$deleted = Order::whereIn('uuid', $ids)->delete();
|
|
334
334
|
|
|
@@ -351,7 +351,7 @@ class OrderController extends FleetOpsController
|
|
|
351
351
|
*/
|
|
352
352
|
public function bulkCancel(BulkActionRequest $request)
|
|
353
353
|
{
|
|
354
|
-
/** @var
|
|
354
|
+
/** @var Order */
|
|
355
355
|
$orders = Order::whereIn('uuid', $request->input('ids'))->get();
|
|
356
356
|
|
|
357
357
|
$count = $orders->count();
|
|
@@ -392,7 +392,7 @@ class OrderController extends FleetOpsController
|
|
|
392
392
|
*/
|
|
393
393
|
public function bulkDispatch(BulkDispatchRequest $request)
|
|
394
394
|
{
|
|
395
|
-
/** @var
|
|
395
|
+
/** @var Order */
|
|
396
396
|
$orders = Order::whereIn('uuid', $request->input('ids'))->get();
|
|
397
397
|
|
|
398
398
|
$count = $orders->count();
|
|
@@ -512,7 +512,7 @@ class OrderController extends FleetOpsController
|
|
|
512
512
|
*/
|
|
513
513
|
public function cancel(CancelOrderRequest $request)
|
|
514
514
|
{
|
|
515
|
-
/** @var
|
|
515
|
+
/** @var Order */
|
|
516
516
|
$order = Order::where('uuid', $request->input('order'))->first();
|
|
517
517
|
|
|
518
518
|
$order->cancel();
|
|
@@ -534,7 +534,7 @@ class OrderController extends FleetOpsController
|
|
|
534
534
|
public function dispatchOrder(Request $request)
|
|
535
535
|
{
|
|
536
536
|
/**
|
|
537
|
-
* @var
|
|
537
|
+
* @var Order
|
|
538
538
|
*/
|
|
539
539
|
$order = Order::select(['uuid', 'driver_assigned_uuid', 'order_config_uuid', 'adhoc', 'dispatched', 'dispatched_at'])->where('uuid', $request->input('order'))->withoutGlobalScopes()->first();
|
|
540
540
|
if (!$order) {
|
|
@@ -909,11 +909,18 @@ class OrderController extends FleetOpsController
|
|
|
909
909
|
return response()->error('Unable to retrieve proof of delivery for subject.');
|
|
910
910
|
}
|
|
911
911
|
|
|
912
|
-
$
|
|
912
|
+
$proofsQuery = Proof::where([
|
|
913
913
|
'company_uuid' => session('company'),
|
|
914
914
|
'order_uuid' => $order->uuid,
|
|
915
|
-
|
|
916
|
-
|
|
915
|
+
]);
|
|
916
|
+
|
|
917
|
+
// if subject is not the order then filter by subject
|
|
918
|
+
if ($order->uuid !== $subject->uuid) {
|
|
919
|
+
$proofsQuery->where('subject_uuid', $subject->uuid);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// get proofs
|
|
923
|
+
$proofs = $proofsQuery->get();
|
|
917
924
|
|
|
918
925
|
return ProofResource::collection($proofs);
|
|
919
926
|
}
|