@fleetbase/fleetops-engine 0.6.8 → 0.6.10

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 (73) hide show
  1. package/addon/components/activity-event-selector.js +4 -0
  2. package/addon/components/admin/navigator-app.js +1 -1
  3. package/addon/components/custom-field-form-panel.js +1 -1
  4. package/addon/components/display-place.hbs +27 -3
  5. package/addon/components/driver-onboard-settings.js +1 -1
  6. package/addon/components/driver-panel/orders.js +1 -1
  7. package/addon/components/edit-order-route-panel.js +1 -1
  8. package/addon/components/entity-field-editing-settings.js +1 -1
  9. package/addon/components/fleet-driver-listing.js +1 -2
  10. package/addon/components/fleet-panel/driver-listing.js +1 -2
  11. package/addon/components/fleet-panel/vehicle-listing.js +1 -2
  12. package/addon/components/fleet-vehicle-listing.js +1 -2
  13. package/addon/components/layout/fleet-ops-sidebar/driver-listing.js +1 -1
  14. package/addon/components/layout/fleet-ops-sidebar/fleet-listing.js +1 -1
  15. package/addon/components/live-map.js +1 -1
  16. package/addon/components/map-container/toolbar/zones-panel.js +1 -1
  17. package/addon/components/order-config-manager/activity-flow.js +1 -1
  18. package/addon/components/order-config-manager/custom-fields.js +1 -1
  19. package/addon/components/order-config-manager/entities.js +1 -1
  20. package/addon/components/order-config-manager.js +1 -1
  21. package/addon/components/route-list.hbs +3 -3
  22. package/addon/components/vendor-panel/drivers.js +1 -2
  23. package/addon/components/widget/fleet-ops-key-metrics.js +1 -1
  24. package/addon/controllers/management/contacts/index.js +1 -2
  25. package/addon/controllers/management/drivers/index.js +1 -2
  26. package/addon/controllers/management/fleets/index.js +1 -2
  27. package/addon/controllers/management/fuel-reports/index.js +1 -2
  28. package/addon/controllers/management/issues/index.js +1 -2
  29. package/addon/controllers/management/vehicles/index.js +1 -2
  30. package/addon/controllers/management/vendors/index.js +1 -2
  31. package/addon/controllers/operations/orders/index/new.js +40 -48
  32. package/addon/controllers/operations/orders/index/view.js +1 -1
  33. package/addon/controllers/operations/orders/index.js +1 -6
  34. package/addon/controllers/operations/service-rates/index.js +1 -2
  35. package/addon/routes/operations/orders/index/view.js +18 -1
  36. package/addon/services/movement-tracker.js +1 -1
  37. package/addon/services/order-creation.js +1 -1
  38. package/addon/styles/fleetops-engine.css +7 -0
  39. package/addon/templates/operations/orders/index/new.hbs +2 -2
  40. package/addon/templates/operations/orders/index/view.hbs +5 -1
  41. package/addon/templates/operations/orders/index.hbs +6 -1
  42. package/addon/templates/settings/notifications.hbs +9 -1
  43. package/composer.json +1 -1
  44. package/extension.json +1 -1
  45. package/package.json +2 -3
  46. package/server/src/Console/Commands/TrackOrderDistanceAndTime.php +2 -2
  47. package/server/src/Events/OrderCanceled.php +6 -0
  48. package/server/src/Events/OrderCompleted.php +6 -0
  49. package/server/src/Events/OrderDispatched.php +6 -0
  50. package/server/src/Events/OrderFailed.php +6 -0
  51. package/server/src/Events/WaypointActivityChanged.php +119 -0
  52. package/server/src/Events/WaypointCompleted.php +119 -0
  53. package/server/src/Flow/Activity.php +28 -10
  54. package/server/src/Flow/Event.php +26 -2
  55. package/server/src/Http/Controllers/Api/v1/DriverController.php +19 -2
  56. package/server/src/Http/Controllers/Api/v1/OrderController.php +274 -164
  57. package/server/src/Http/Controllers/Internal/v1/OrderController.php +15 -8
  58. package/server/src/Http/Filter/OrderFilter.php +4 -4
  59. package/server/src/Http/Resources/v1/Payload.php +1 -1
  60. package/server/src/Listeners/HandleOrderCanceled.php +0 -10
  61. package/server/src/Listeners/NotifyOrderEvent.php +4 -4
  62. package/server/src/Models/OrderConfig.php +50 -35
  63. package/server/src/Models/Payload.php +26 -7
  64. package/server/src/Models/Waypoint.php +10 -0
  65. package/server/src/Notifications/OrderAssigned.php +3 -5
  66. package/server/src/Notifications/OrderCanceled.php +32 -12
  67. package/server/src/Notifications/OrderCompleted.php +31 -11
  68. package/server/src/Notifications/OrderDispatchFailed.php +3 -5
  69. package/server/src/Notifications/OrderDispatched.php +31 -11
  70. package/server/src/Notifications/OrderFailed.php +32 -12
  71. package/server/src/Notifications/OrderPing.php +2 -6
  72. package/server/src/Notifications/OrderSplit.php +1 -1
  73. 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
- // Trigger order created event
324
- event(new OrderReady($order));
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()->json(
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('No resource not found.');
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 ($order->payload->isMultipleDropOrder && !$order->payload->current_waypoint_uuid) {
999
+ if ($isMultipleWaypointOrder && !$order->payload->current_waypoint_uuid) {
993
1000
  $order->payload->setFirstWaypoint($activity, $location);
994
1001
  }
995
1002
 
996
- if (Utils::isActivity($activity) && $activity->completesOrder() && $order->payload->isMultipleDropOrder) {
997
- // confirm every waypoint is completed
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 (!$order->payload->isMultipleDropOrder) {
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
- $activities = $order->config()->nextActivity();
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::with(['payload.waypoints', 'payload.pickup', 'payload.dropoff'])->findRecordOrFail($id);
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
- $subject = $type === null ? $order : null;
1260
-
1261
- switch ($type) {
1262
- case 'place':
1263
- case 'waypoint':
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
- $subject = $type === null ? $order : null;
1330
-
1331
- switch ($type) {
1332
- case 'place':
1333
- case 'waypoint':
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
- * Validate a photo.
1381
+ * Capture one or more photos for an order (as proof) and persist them.
1396
1382
  *
1397
- * @return void
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
- $disk = $request->input('disk', config('filesystems.default'));
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
- $order = Order::findRecordOrFail($id);
1412
- } catch (ModelNotFoundException $e) {
1413
- return response()->apiError('Order resource not found.', 404);
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
- if (!$photos) {
1417
- return response()->apiError('No photo data to capture.');
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
- $subject = $type === null ? $order : null;
1428
+ return;
1429
+ }
1421
1430
 
1422
- switch ($type) {
1423
- case 'place':
1424
- case 'waypoint':
1425
- $subject = Waypoint::where('payload_uuid', $order->payload_uuid)->where(function ($q) use ($subjectId) {
1426
- $q->whereHas('place', function ($q) use ($subjectId) {
1427
- $q->where('public_id', $subjectId);
1428
- });
1429
- $q->orWhere('public_id', $subjectId);
1430
- })->withoutGlobalScopes()->first();
1431
- break;
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
- case 'entity':
1434
- $subject = Entity::where('public_id', $subjectId)->withoutGlobalScopes()->first();
1435
- break;
1470
+ if (empty($incoming)) {
1471
+ return response()->apiError('No photo data to capture.');
1472
+ }
1436
1473
 
1437
- case 'order':
1438
- default:
1439
- $subject = $order;
1440
- break;
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
- foreach ($photos as $photo) {
1448
- // create proof instance
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' => $photo,
1495
+ 'raw_data' => $item instanceof UploadedFile ? null : $item,
1456
1496
  'data' => $data,
1457
1497
  ]);
1458
1498
 
1459
- $path = implode('/', ['uploads', session('company'), 'photos', $proof->public_id . '.png']);
1460
- // $path = 'uploads/' . session('company') . '/photos/' . $proof->public_id . '.png';
1461
- Storage::disk($disk)->put($path, base64_decode($photo));
1462
- $file = File::create([
1463
- 'company_uuid' => session('company'),
1464
- 'uploader_uuid' => session('user'),
1465
- 'name' => basename($path),
1466
- 'original_filename' => basename($path),
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 = strtok($subjectId, '_');
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
- $proofs = Proof::where([
1627
+ $proofsQuery = Proof::where([
1530
1628
  'company_uuid' => session('company'),
1531
1629
  'order_uuid' => $order->uuid,
1532
- 'subject_uuid' => $subject->uuid,
1533
- ])->get();
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 \Fleetbase\Models\Order */
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 \Fleetbase\Models\Order */
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 \Fleetbase\Models\Order */
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 \Fleetbase\Models\Order */
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 \Fleetbase\Models\Order
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
- $proofs = Proof::where([
912
+ $proofsQuery = Proof::where([
913
913
  'company_uuid' => session('company'),
914
914
  'order_uuid' => $order->uuid,
915
- 'subject_uuid' => $subject->uuid,
916
- ])->get();
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
  }