@fleetbase/fleetops-engine 0.6.38 → 0.6.39
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/form.hbs +2 -12
- package/addon/components/activity/logic-builder.hbs +1 -1
- package/addon/components/custom-entity/form.hbs +4 -20
- package/addon/components/device/form.hbs +3 -15
- package/addon/components/device/panel-header.hbs +13 -2
- package/addon/components/driver/details.hbs +33 -2
- package/addon/components/driver/form.hbs +42 -0
- package/addon/components/driver/panel-header.hbs +8 -1
- package/addon/components/driver/schedule.hbs +115 -76
- package/addon/components/driver/schedule.js +349 -157
- package/addon/components/driver-onboard-settings.hbs +2 -8
- package/addon/components/entity-field-editing-settings.hbs +2 -5
- package/addon/components/equipment/card.hbs +49 -0
- package/addon/components/equipment/card.js +6 -0
- package/addon/components/equipment/details.hbs +83 -44
- package/addon/components/equipment/form.hbs +111 -41
- package/addon/components/equipment/form.js +78 -10
- package/addon/components/equipment/panel-header.hbs +36 -0
- package/addon/components/equipment/panel-header.js +2 -0
- package/addon/components/fleet/driver-listing.hbs +3 -1
- package/addon/components/fleet/vehicle-listing.hbs +4 -7
- package/addon/components/fleet-panel/vehicle-listing.hbs +1 -6
- package/addon/components/fuel-report/form.hbs +1 -5
- package/addon/components/layout/fleet-ops-sidebar.hbs +40 -36
- package/addon/components/layout/fleet-ops-sidebar.js +61 -10
- package/addon/components/maintenance/cost-panel.hbs +176 -0
- package/addon/components/maintenance/cost-panel.js +241 -0
- package/addon/components/maintenance/details.hbs +123 -60
- package/addon/components/maintenance/form.hbs +138 -78
- package/addon/components/maintenance/form.js +131 -6
- package/addon/components/maintenance/panel-header.hbs +31 -0
- package/addon/components/maintenance/panel-header.js +2 -0
- package/addon/components/maintenance-schedule/details.hbs +260 -0
- package/addon/components/maintenance-schedule/details.js +158 -0
- package/addon/components/maintenance-schedule/form.hbs +287 -0
- package/addon/components/maintenance-schedule/form.js +199 -0
- package/addon/components/map/container.hbs +1 -1
- package/addon/components/map/drawer/device-event-listing.hbs +1 -1
- package/addon/components/map/drawer/driver-listing.hbs +1 -6
- package/addon/components/map/drawer/place-listing.hbs +1 -6
- package/addon/components/map/drawer/vehicle-listing.hbs +1 -6
- package/addon/components/map/drawer.hbs +8 -1
- package/addon/components/map/leaflet-live-map.hbs +2 -1
- package/addon/components/map/toolbar/visibility-control-panel.hbs +1 -1
- package/addon/components/map/toolbar.hbs +4 -29
- package/addon/components/modals/add-driver-shift.hbs +155 -0
- package/addon/components/modals/add-driver-shift.js +210 -0
- package/addon/components/modals/bulk-assign-orders.hbs +67 -0
- package/addon/components/modals/driver-shift.hbs +43 -0
- package/addon/components/modals/driver-shift.js +56 -0
- package/addon/components/modals/entity-form.hbs +5 -28
- package/addon/components/modals/orchestrator-import.hbs +351 -0
- package/addon/components/modals/orchestrator-import.js +807 -0
- package/addon/components/modals/order-config-new-status.hbs +1 -5
- package/addon/components/modals/scheduling-conflict.hbs +47 -0
- package/addon/components/modals/scheduling-conflict.js +53 -0
- package/addon/components/modals/send-work-order.hbs +91 -0
- package/addon/components/modals/send-work-order.js +3 -0
- package/addon/components/modals/service-area-form.hbs +1 -6
- package/addon/components/modals/set-driver-availability.hbs +50 -0
- package/addon/components/modals/set-driver-availability.js +57 -0
- package/addon/components/modals/update-order-activity.hbs +13 -9
- package/addon/components/modals/user-form.hbs +1 -5
- package/addon/components/modals/vehicle-form.hbs +17 -102
- package/addon/components/modals/vendor-form.hbs +15 -82
- package/addon/components/orchestrator/card-fields-settings.hbs +76 -0
- package/addon/components/orchestrator/card-fields-settings.js +134 -0
- package/addon/components/orchestrator/order-pool.hbs +264 -0
- package/addon/components/orchestrator/order-pool.js +394 -0
- package/addon/components/orchestrator/phase-builder.hbs +162 -0
- package/addon/components/orchestrator/phase-builder.js +162 -0
- package/addon/components/orchestrator/plan-viewer.hbs +278 -0
- package/addon/components/orchestrator/plan-viewer.js +342 -0
- package/addon/components/orchestrator/resource-panel.hbs +301 -0
- package/addon/components/orchestrator/resource-panel.js +106 -0
- package/addon/components/orchestrator-workbench.hbs +318 -0
- package/addon/components/orchestrator-workbench.js +1087 -0
- package/addon/components/order/details/custom-fields.hbs +10 -1
- package/addon/components/order/details/detail.hbs +37 -2
- package/addon/components/order/details/detail.js +0 -1
- package/addon/components/order/details/integrated-vendor-details.hbs +4 -4
- package/addon/components/order/details/notes.hbs +1 -7
- package/addon/components/order/details/route.hbs +1 -5
- package/addon/components/order/form/details.hbs +44 -10
- package/addon/components/order/form/details.js +57 -0
- package/addon/components/order/form/notes.hbs +1 -7
- package/addon/components/order/form/payload.hbs +1 -7
- package/addon/components/order/form/route.hbs +3 -15
- package/addon/components/order/header.hbs +1 -7
- package/addon/components/order/route-editor.hbs +4 -25
- package/addon/components/order/schedule-card.hbs +102 -95
- package/addon/components/order/schedule-card.js +8 -3
- package/addon/components/order-config-manager/details.hbs +2 -10
- package/addon/components/order-config-manager/entities.hbs +1 -7
- package/addon/components/order-config-manager.hbs +1 -6
- package/addon/components/part/card.hbs +49 -0
- package/addon/components/part/card.js +6 -0
- package/addon/components/part/details.hbs +102 -56
- package/addon/components/part/form.hbs +131 -56
- package/addon/components/part/form.js +78 -11
- package/addon/components/part/panel-header.hbs +36 -0
- package/addon/components/part/panel-header.js +2 -0
- package/addon/components/place/form.hbs +1 -7
- package/addon/components/sensor/details.hbs +1 -1
- package/addon/components/sensor/form.hbs +5 -3
- package/addon/components/sensor/panel-header.hbs +8 -1
- package/addon/components/service-area/form.hbs +1 -6
- package/addon/components/service-rate/form.hbs +12 -60
- package/addon/components/telematic/form.hbs +6 -2
- package/addon/components/vehicle/details/maintenance-history.hbs +42 -0
- package/addon/components/vehicle/details/maintenance-history.js +32 -0
- package/addon/components/vehicle/details/schedules.hbs +40 -0
- package/addon/components/vehicle/details/schedules.js +32 -0
- package/addon/components/vehicle/details/work-orders.hbs +42 -0
- package/addon/components/vehicle/details/work-orders.js +32 -0
- package/addon/components/vehicle/details.hbs +30 -0
- package/addon/components/vehicle/form.hbs +39 -0
- package/addon/components/vehicle/panel-header.hbs +19 -49
- package/addon/components/warranty/details.hbs +3 -2
- package/addon/components/warranty/form.hbs +3 -17
- package/addon/components/work-order/details.hbs +135 -40
- package/addon/components/work-order/form.hbs +178 -45
- package/addon/components/work-order/form.js +197 -4
- package/addon/components/work-order/panel-header.hbs +31 -0
- package/addon/components/work-order/panel-header.js +2 -0
- package/addon/controllers/connectivity/devices/index/details.js +1 -1
- package/addon/controllers/maintenance/equipment/index/details/index.js +0 -1
- package/addon/controllers/maintenance/equipment/index/details.js +36 -1
- package/addon/controllers/maintenance/equipment/index/edit.js +56 -1
- package/addon/controllers/maintenance/equipment/index/new.js +32 -1
- package/addon/controllers/maintenance/equipment/index.js +127 -113
- package/addon/controllers/maintenance/maintenances/index/details/index.js +3 -0
- package/addon/controllers/maintenance/maintenances/index/details.js +54 -0
- package/addon/controllers/maintenance/maintenances/index/edit.js +68 -0
- package/addon/controllers/maintenance/maintenances/index/new.js +34 -0
- package/addon/controllers/maintenance/maintenances/index.js +191 -0
- package/addon/controllers/maintenance/parts/index/details/index.js +0 -1
- package/addon/controllers/maintenance/parts/index/details.js +36 -1
- package/addon/controllers/maintenance/parts/index/edit.js +56 -1
- package/addon/controllers/maintenance/parts/index/new.js +32 -1
- package/addon/controllers/maintenance/parts/index.js +135 -113
- package/addon/controllers/maintenance/schedules/index/details.js +115 -0
- package/addon/controllers/maintenance/schedules/index/edit.js +41 -0
- package/addon/controllers/maintenance/schedules/index/new.js +33 -0
- package/addon/controllers/maintenance/schedules/index.js +280 -0
- package/addon/controllers/maintenance/work-orders/index/details.js +41 -1
- package/addon/controllers/maintenance/work-orders/index/edit.js +67 -1
- package/addon/controllers/maintenance/work-orders/index/new.js +43 -1
- package/addon/controllers/maintenance/work-orders/index.js +105 -113
- package/addon/controllers/management/drivers/index/details.js +6 -1
- package/addon/controllers/management/vehicles/index/details.js +65 -0
- package/addon/controllers/management/vehicles/index.js +18 -0
- package/addon/controllers/operations/orchestrator.js +10 -0
- package/addon/controllers/operations/orders/index.js +6 -0
- package/addon/controllers/operations/scheduler/fleet-schedule.js +341 -0
- package/addon/controllers/operations/scheduler/index.js +799 -275
- package/addon/controllers/operations/scheduler.js +21 -0
- package/addon/controllers/settings/orchestrator.js +70 -0
- package/addon/controllers/settings/scheduling.js +155 -0
- package/addon/extension.js +19 -0
- package/addon/instance-initializers/register-vroom-allocation.js +27 -0
- package/addon/models/maintenance-schedule.js +61 -0
- package/addon/routes/maintenance/equipment/index/details.js +27 -1
- package/addon/routes/maintenance/equipment/index/edit.js +27 -1
- package/addon/routes/maintenance/maintenances/index/details/index.js +3 -0
- package/addon/routes/maintenance/maintenances/index/details.js +29 -0
- package/addon/routes/maintenance/maintenances/index/edit.js +29 -0
- package/addon/routes/maintenance/maintenances/index/new.js +3 -0
- package/addon/routes/maintenance/maintenances/index.js +23 -0
- package/addon/routes/maintenance/maintenances.js +3 -0
- package/addon/routes/maintenance/parts/index/details.js +27 -1
- package/addon/routes/maintenance/parts/index/edit.js +27 -1
- package/addon/routes/maintenance/schedules/index/details/index.js +2 -0
- package/addon/routes/maintenance/schedules/index/details/work-orders.js +11 -0
- package/addon/routes/maintenance/schedules/index/details.js +25 -0
- package/addon/routes/maintenance/schedules/index/edit.js +25 -0
- package/addon/routes/maintenance/schedules/index/new.js +2 -0
- package/addon/routes/maintenance/schedules/index.js +21 -0
- package/addon/routes/maintenance/schedules.js +2 -0
- package/addon/routes/maintenance/work-orders/index/details.js +27 -1
- package/addon/routes/maintenance/work-orders/index/edit.js +27 -1
- package/addon/routes/management/vehicles/index/details/maintenance-history.js +3 -0
- package/addon/routes/management/vehicles/index/details/schedules.js +3 -0
- package/addon/routes/management/vehicles/index/details/work-orders.js +3 -0
- package/addon/routes/operations/orchestrator.js +23 -0
- package/addon/routes/operations/scheduler/fleet-schedule.js +28 -0
- package/addon/routes/operations/scheduler/index.js +48 -26
- package/addon/routes/operations/scheduler.js +14 -1
- package/addon/routes/settings/orchestrator.js +27 -0
- package/addon/routes/settings/scheduling.js +3 -0
- package/addon/routes.js +31 -1
- package/addon/services/driver-actions.js +40 -7
- package/addon/services/driver-scheduling.js +4 -1
- package/addon/services/equipment-actions.js +15 -5
- package/addon/services/leaflet-map-manager.js +14 -6
- package/addon/services/maintenance-actions.js +17 -14
- package/addon/services/maintenance-schedule-actions.js +118 -0
- package/addon/services/orchestration-engine-interface.js +49 -0
- package/addon/services/orchestration-engine.js +74 -0
- package/addon/services/order-actions.js +15 -0
- package/addon/services/order-allocation.js +116 -0
- package/addon/services/part-actions.js +12 -2
- package/addon/services/scheduling.js +316 -0
- package/addon/services/vehicle-actions.js +70 -7
- package/addon/services/vroom-allocation-engine.js +45 -0
- package/addon/services/work-order-actions.js +80 -0
- package/addon/styles/fleetops-engine.css +1658 -0
- package/addon/templates/analytics/reports/index/edit.hbs +1 -1
- package/addon/templates/analytics/reports/index/new.hbs +1 -1
- package/addon/templates/application.hbs +6 -1
- package/addon/templates/connectivity/devices/index/details/events.hbs +0 -1
- package/addon/templates/connectivity/devices.hbs +0 -1
- package/addon/templates/connectivity/events/index/details.hbs +0 -1
- package/addon/templates/connectivity/events.hbs +0 -1
- package/addon/templates/connectivity/sensors.hbs +0 -1
- package/addon/templates/connectivity/telematics/index/details/devices.hbs +0 -1
- package/addon/templates/connectivity/telematics/index/details/events.hbs +0 -1
- package/addon/templates/connectivity/telematics/index/details/sensors.hbs +0 -1
- package/addon/templates/connectivity/telematics.hbs +0 -1
- package/addon/templates/connectivity/tracking.hbs +0 -1
- package/addon/templates/connectivity.hbs +0 -1
- package/addon/templates/maintenance/equipment/index/details/index.hbs +1 -2
- package/addon/templates/maintenance/equipment/index/details.hbs +15 -2
- package/addon/templates/maintenance/equipment/index/edit.hbs +12 -2
- package/addon/templates/maintenance/equipment/index/new.hbs +1 -2
- package/addon/templates/maintenance/equipment/index.hbs +48 -13
- package/addon/templates/maintenance/equipment.hbs +0 -1
- package/addon/templates/maintenance/maintenances/index/details/index.hbs +1 -0
- package/addon/templates/maintenance/maintenances/index/details.hbs +15 -0
- package/addon/templates/maintenance/maintenances/index/edit.hbs +12 -0
- package/addon/templates/maintenance/maintenances/index/new.hbs +11 -0
- package/addon/templates/maintenance/maintenances/index.hbs +14 -0
- package/addon/templates/maintenance/maintenances.hbs +1 -0
- package/addon/templates/maintenance/parts/index/details/index.hbs +1 -2
- package/addon/templates/maintenance/parts/index/details.hbs +15 -2
- package/addon/templates/maintenance/parts/index/edit.hbs +12 -2
- package/addon/templates/maintenance/parts/index/new.hbs +1 -2
- package/addon/templates/maintenance/parts/index.hbs +48 -13
- package/addon/templates/maintenance/parts.hbs +0 -1
- package/addon/templates/maintenance/schedules/index/details/index.hbs +1 -0
- package/addon/templates/maintenance/schedules/index/details/work-orders.hbs +39 -0
- package/addon/templates/maintenance/schedules/index/details.hbs +14 -0
- package/addon/templates/maintenance/schedules/index/edit.hbs +12 -0
- package/addon/templates/maintenance/schedules/index/new.hbs +11 -0
- package/addon/templates/maintenance/schedules/index.hbs +40 -0
- package/addon/templates/maintenance/schedules.hbs +1 -0
- package/addon/templates/maintenance/work-orders/index/details.hbs +2 -1
- package/addon/templates/maintenance/work-orders/index/edit.hbs +2 -4
- package/addon/templates/maintenance/work-orders/index/new.hbs +1 -2
- package/addon/templates/maintenance/work-orders.hbs +0 -1
- package/addon/templates/management/contacts/customers/edit.hbs +1 -2
- package/addon/templates/management/contacts/customers/new.hbs +1 -2
- package/addon/templates/management/contacts/customers.hbs +1 -1
- package/addon/templates/management/contacts/index/edit.hbs +1 -2
- package/addon/templates/management/contacts/index/new.hbs +1 -2
- package/addon/templates/management/drivers/index/details/orders.hbs +0 -1
- package/addon/templates/management/drivers/index/edit.hbs +1 -2
- package/addon/templates/management/drivers/index/new.hbs +1 -2
- package/addon/templates/management/fleets/index/edit.hbs +1 -2
- package/addon/templates/management/fleets/index/new.hbs +1 -2
- package/addon/templates/management/fleets/index.hbs +1 -2
- package/addon/templates/management/fuel-reports/index/edit.hbs +1 -2
- package/addon/templates/management/fuel-reports/index/new.hbs +1 -2
- package/addon/templates/management/fuel-reports/index.hbs +1 -2
- package/addon/templates/management/issues/index/edit.hbs +1 -2
- package/addon/templates/management/issues/index/new.hbs +1 -2
- package/addon/templates/management/issues/index.hbs +1 -2
- package/addon/templates/management/places/index/details/activity.hbs +0 -1
- package/addon/templates/management/places/index/details/comments.hbs +0 -1
- package/addon/templates/management/places/index/details/documents.hbs +0 -1
- package/addon/templates/management/places/index/details/map.hbs +0 -1
- package/addon/templates/management/places/index/details/operations.hbs +0 -1
- package/addon/templates/management/places/index/details/performance.hbs +0 -1
- package/addon/templates/management/places/index/details/rules.hbs +0 -1
- package/addon/templates/management/vehicles/index/details/equipment.hbs +0 -1
- package/addon/templates/management/vehicles/index/details/maintenance-history.hbs +2 -0
- package/addon/templates/management/vehicles/index/details/schedules.hbs +2 -0
- package/addon/templates/management/vehicles/index/details/work-orders.hbs +2 -0
- package/addon/templates/management/vehicles/index/details.hbs +1 -1
- package/addon/templates/management/vehicles/index/edit.hbs +1 -2
- package/addon/templates/management/vehicles/index/new.hbs +1 -2
- package/addon/templates/management/vendors/index/edit.hbs +1 -2
- package/addon/templates/management/vendors/index/new.hbs +1 -2
- package/addon/templates/management/vendors/index.hbs +1 -2
- package/addon/templates/management/vendors/integrated.hbs +1 -2
- package/addon/templates/operations/orchestrator.hbs +1 -0
- package/addon/templates/operations/orders/index.hbs +6 -1
- package/addon/templates/operations/scheduler/fleet-schedule.hbs +41 -0
- package/addon/templates/operations/scheduler/index.hbs +147 -88
- package/addon/templates/operations/scheduler.hbs +7 -1
- package/addon/templates/settings/avatars.hbs +1 -1
- package/addon/templates/settings/orchestrator.hbs +65 -0
- package/addon/templates/settings/payments/index.hbs +1 -5
- package/addon/templates/settings/scheduling.hbs +82 -0
- package/addon/utils/create-full-calendar-event-from-order.js +52 -14
- package/addon/utils/create-full-calendar-event-from-schedule-item.js +50 -0
- package/addon/utils/fleet-ops-options.js +254 -0
- package/addon/utils/route-colors.js +99 -0
- package/addon/utils/to-calendar-date.js +70 -0
- package/app/components/driver/schedule.js +1 -0
- package/app/components/maintenance/cost-panel.js +1 -0
- package/app/components/maintenance/panel-header.js +1 -0
- package/app/components/maintenance-schedule/details.js +1 -0
- package/app/components/maintenance-schedule/form.js +1 -0
- package/app/components/modals/add-driver-shift.js +1 -0
- package/app/components/modals/bulk-assign-orders.js +1 -0
- package/app/components/modals/driver-shift.js +1 -0
- package/app/components/modals/orchestrator-import.js +1 -0
- package/app/components/modals/scheduling-conflict.js +1 -0
- package/app/components/modals/send-work-order.js +1 -0
- package/app/components/modals/set-driver-availability.js +1 -0
- package/app/components/orchestrator/card-fields-settings.js +1 -0
- package/app/components/orchestrator/order-pool.js +1 -0
- package/app/components/orchestrator/phase-builder.js +1 -0
- package/app/components/orchestrator/plan-viewer.js +1 -0
- package/app/components/orchestrator/resource-panel.js +1 -0
- package/app/components/orchestrator-workbench.js +1 -0
- package/app/components/vehicle/details/maintenance-history.js +1 -0
- package/app/components/vehicle/details/schedules.js +1 -0
- package/app/components/vehicle/details/work-orders.js +1 -0
- package/app/controllers/operations/orchestrator.js +1 -0
- package/app/controllers/settings/orchestrator.js +1 -0
- package/app/controllers/settings/scheduling.js +1 -0
- package/app/routes/operations/orchestrator.js +1 -0
- package/app/routes/settings/orchestrator.js +1 -0
- package/app/routes/settings/scheduling.js +1 -0
- package/app/services/maintenance-schedule-actions.js +1 -0
- package/app/services/orchestration-engine-interface.js +1 -0
- package/app/services/orchestration-engine.js +1 -0
- package/app/services/order-allocation.js +1 -0
- package/app/services/scheduling.js +1 -0
- package/app/services/vroom-allocation-engine.js +1 -0
- package/app/templates/settings/scheduling.js +1 -0
- package/app/utils/create-full-calendar-event-from-schedule-item.js +1 -0
- package/app/utils/route-colors.js +1 -0
- package/composer.json +5 -3
- package/extension.json +1 -1
- package/package.json +6 -5
- package/server/config/fleetops.php +20 -1
- package/server/migrations/2025_08_28_054927_create_parts_table.php +2 -2
- package/server/migrations/2025_08_28_054932_add_public_id_to_maintenance_tables.php +45 -0
- package/server/migrations/2025_09_01_000001_create_maintenance_schedules_table.php +88 -0
- package/server/migrations/2026_04_01_000001_fix_monetary_columns_in_parts_table.php +48 -0
- package/server/migrations/2026_04_01_000003_add_photo_uuid_to_equipment_and_parts_tables.php +61 -0
- package/server/migrations/2026_04_01_000004_add_public_id_to_equipments_table.php +38 -0
- package/server/migrations/2026_04_01_000005_add_missing_columns_to_parts_table.php +67 -0
- package/server/migrations/2026_04_04_000001_add_reminder_offsets_to_maintenance_schedules.php +44 -0
- package/server/migrations/2026_04_08_000001_add_orchestrator_columns_to_vehicles_table.php +58 -0
- package/server/migrations/2026_04_08_000002_add_orchestrator_columns_to_drivers_table.php +41 -0
- package/server/migrations/2026_04_08_000003_add_orchestrator_columns_to_orders_table.php +38 -0
- package/server/migrations/2026_04_08_000004_add_orchestrator_columns_to_payloads_table.php +41 -0
- package/server/migrations/2026_04_08_000005_add_orchestrator_columns_to_waypoints_table.php +38 -0
- package/server/migrations/2026_04_09_000001_create_manifests_table.php +48 -0
- package/server/migrations/2026_04_09_000002_create_manifest_stops_table.php +48 -0
- package/server/migrations/2026_04_09_000003_add_manifest_uuid_to_orders_table.php +28 -0
- package/server/migrations/2026_04_13_000001_add_pod_notes_columns_to_waypoints_table.php +39 -0
- package/server/resources/views/mail/maintenance-schedule-reminder.blade.php +59 -0
- package/server/resources/views/mail/work-order-dispatched.blade.php +67 -0
- package/server/src/Auth/Schemas/FleetOps.php +44 -0
- package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +150 -0
- package/server/src/Console/Commands/SendMaintenanceReminders.php +128 -0
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +1 -0
- package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +27 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +9 -2
- package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +165 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +304 -0
- package/server/src/Http/Controllers/Internal/v1/ManifestController.php +138 -0
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +975 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +42 -0
- package/server/src/Http/Controllers/Internal/v1/PartController.php +27 -0
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +118 -0
- package/server/src/Http/Controllers/Internal/v1/Traits/DriverSchedulingTrait.php +214 -0
- package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +68 -0
- package/server/src/Http/Resources/v1/Driver.php +1 -0
- package/server/src/Http/Resources/v1/Maintenance.php +138 -0
- package/server/src/Http/Resources/v1/MaintenanceSchedule.php +137 -0
- package/server/src/Http/Resources/v1/Orchestrator/Order.php +116 -0
- package/server/src/Http/Resources/v1/Order.php +7 -4
- package/server/src/Http/Resources/v1/Waypoint.php +7 -0
- package/server/src/Http/Resources/v1/WorkOrder.php +136 -0
- package/server/src/Imports/EquipmentImport.php +32 -0
- package/server/src/Imports/MaintenanceImport.php +32 -0
- package/server/src/Imports/MaintenanceScheduleImport.php +32 -0
- package/server/src/Imports/PartImport.php +32 -0
- package/server/src/Imports/WorkOrderImport.php +32 -0
- package/server/src/Jobs/ProcessAllocationJob.php +119 -0
- package/server/src/Listeners/HandleDeliveryCompletion.php +47 -0
- package/server/src/Listeners/NotifyDriverOnShiftChange.php +63 -0
- package/server/src/Mail/MaintenanceScheduleReminder.php +68 -0
- package/server/src/Mail/WorkOrderDispatched.php +58 -0
- package/server/src/Models/Asset.php +2 -2
- package/server/src/Models/Device.php +1 -1
- package/server/src/Models/Driver.php +82 -4
- package/server/src/Models/Equipment.php +62 -2
- package/server/src/Models/Maintenance.php +127 -9
- package/server/src/Models/MaintenanceSchedule.php +353 -0
- package/server/src/Models/Manifest.php +214 -0
- package/server/src/Models/ManifestStop.php +162 -0
- package/server/src/Models/Order.php +70 -0
- package/server/src/Models/OrderConfig.php +5 -2
- package/server/src/Models/Part.php +69 -3
- package/server/src/Models/Payload.php +7 -2
- package/server/src/Models/Place.php +1 -1
- package/server/src/Models/Sensor.php +1 -1
- package/server/src/Models/ServiceQuote.php +1 -1
- package/server/src/Models/Vehicle.php +20 -1
- package/server/src/Models/Warranty.php +1 -1
- package/server/src/Models/Waypoint.php +7 -1
- package/server/src/Models/WorkOrder.php +122 -12
- package/server/src/Notifications/DriverShiftChanged.php +110 -0
- package/server/src/Observers/WorkOrderObserver.php +107 -0
- package/server/src/Orchestration/Contracts/OrchestrationEngineInterface.php +63 -0
- package/server/src/Orchestration/Engines/DriverAssignmentEngine.php +265 -0
- package/server/src/Orchestration/Engines/GreedyOrchestrationEngine.php +155 -0
- package/server/src/Orchestration/Engines/RouteSequencingEngine.php +272 -0
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +192 -0
- package/server/src/Orchestration/OrchestrationEngineRegistry.php +83 -0
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +290 -0
- package/server/src/Providers/EventServiceProvider.php +7 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +42 -15
- package/server/src/routes.php +65 -4
- package/translations/ar-ae.yml +44 -12
- package/translations/bg-bg.yaml +51 -10
- package/translations/en-us.yaml +444 -1
- package/translations/fr-fr.yaml +51 -10
- package/translations/mn-mn.yaml +51 -10
- package/translations/pt-br.yaml +51 -10
- package/translations/ru-ru.yaml +51 -10
- package/translations/vi-vn.yaml +48 -12
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Http\Resources\v1\Orchestrator\Order as OrchestratorOrderResource;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Contact;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
8
|
+
use Fleetbase\FleetOps\Models\Manifest;
|
|
9
|
+
use Fleetbase\FleetOps\Models\ManifestStop;
|
|
10
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
11
|
+
use Fleetbase\FleetOps\Models\OrderConfig;
|
|
12
|
+
use Fleetbase\FleetOps\Models\Payload;
|
|
13
|
+
use Fleetbase\FleetOps\Models\Place;
|
|
14
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
15
|
+
use Fleetbase\FleetOps\Models\Vendor;
|
|
16
|
+
use Fleetbase\FleetOps\Orchestration\Engines\DriverAssignmentEngine;
|
|
17
|
+
use Fleetbase\FleetOps\Orchestration\Engines\RouteSequencingEngine;
|
|
18
|
+
use Fleetbase\FleetOps\Orchestration\OrchestrationEngineRegistry;
|
|
19
|
+
use Fleetbase\Http\Controllers\Controller;
|
|
20
|
+
use Fleetbase\Models\Setting;
|
|
21
|
+
use Illuminate\Http\JsonResponse;
|
|
22
|
+
use Illuminate\Http\Request;
|
|
23
|
+
use Illuminate\Support\Carbon;
|
|
24
|
+
use Illuminate\Support\Facades\DB;
|
|
25
|
+
use Illuminate\Support\Str;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* OrchestrationController.
|
|
29
|
+
*
|
|
30
|
+
* HTTP interface for the Orchestrator Workbench.
|
|
31
|
+
*
|
|
32
|
+
* Responsibilities:
|
|
33
|
+
* - Serving orders for the workbench (with custom field values)
|
|
34
|
+
* - Running orchestration phases (assign_vehicles, assign_drivers, optimize, optimize_routes, allocate)
|
|
35
|
+
* - Committing a proposed plan to Manifests and ManifestStops
|
|
36
|
+
* - Listing available orchestration engines
|
|
37
|
+
* - Providing order-config custom field definitions for card configuration
|
|
38
|
+
* - Importing orders from parsed CSV/Excel data
|
|
39
|
+
*/
|
|
40
|
+
class OrchestrationController extends Controller
|
|
41
|
+
{
|
|
42
|
+
public function __construct(protected OrchestrationEngineRegistry $registry)
|
|
43
|
+
{
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Return orders for the Orchestrator Workbench.
|
|
48
|
+
*
|
|
49
|
+
* This endpoint uses the dedicated OrchestratorOrderResource which includes
|
|
50
|
+
* custom_field_values — unlike the lightweight Index/Order resource used by
|
|
51
|
+
* the tabular orders view, which intentionally omits them for performance.
|
|
52
|
+
*
|
|
53
|
+
* GET /int/v1/fleet-ops/orchestrator/orders
|
|
54
|
+
*/
|
|
55
|
+
public function orders(Request $request): JsonResponse
|
|
56
|
+
{
|
|
57
|
+
$companyUuid = session('company');
|
|
58
|
+
|
|
59
|
+
$query = Order::where('company_uuid', $companyUuid)->whereIn('status', ['created', 'dispatched', 'started']);
|
|
60
|
+
|
|
61
|
+
$query->whereHas('payload', function ($payloadQuery) {
|
|
62
|
+
$payloadQuery->where(function ($q) {
|
|
63
|
+
$q->whereHas('waypoints', function ($w) {
|
|
64
|
+
$w->whereNotNull('waypoints.uuid');
|
|
65
|
+
});
|
|
66
|
+
$q->orWhereHas('pickup', function ($p) {
|
|
67
|
+
$p->whereNotNull('places.uuid');
|
|
68
|
+
});
|
|
69
|
+
$q->orWhereHas('dropoff', function ($d) {
|
|
70
|
+
$d->whereNotNull('places.uuid');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
$query->whereHas('trackingNumber', function ($q) {
|
|
76
|
+
$q->select('uuid');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
$query->whereHas('trackingStatuses', function ($q) {
|
|
80
|
+
$q->select('uuid');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if ($request->boolean('unassigned')) {
|
|
84
|
+
$query->whereNull('vehicle_assigned_uuid');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
$query->with([
|
|
88
|
+
'payload.entities',
|
|
89
|
+
'payload.waypoints',
|
|
90
|
+
'payload.pickup',
|
|
91
|
+
'payload.dropoff',
|
|
92
|
+
'payload.return',
|
|
93
|
+
'trackingNumber',
|
|
94
|
+
'trackingStatuses',
|
|
95
|
+
'driverAssigned' => function ($query) {
|
|
96
|
+
$query->without(['jobs', 'currentJob']);
|
|
97
|
+
},
|
|
98
|
+
'vehicleAssigned' => function ($query) {
|
|
99
|
+
$query->without(['fleets', 'vendor']);
|
|
100
|
+
},
|
|
101
|
+
'customer',
|
|
102
|
+
'facilitator',
|
|
103
|
+
'customFieldValues.customField',
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
$limit = min((int) $request->input('limit', 500), 1000);
|
|
107
|
+
$orders = $query->limit($limit)->get();
|
|
108
|
+
|
|
109
|
+
return response()->json([
|
|
110
|
+
'orders' => OrchestratorOrderResource::collection($orders)->resolve(),
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run an orchestration phase for the given mode.
|
|
116
|
+
*
|
|
117
|
+
* POST /int/v1/fleet-ops/orchestrator/run
|
|
118
|
+
*/
|
|
119
|
+
public function run(Request $request): JsonResponse
|
|
120
|
+
{
|
|
121
|
+
$companyUuid = session('company');
|
|
122
|
+
$mode = $request->input('mode', 'assign_vehicles');
|
|
123
|
+
$orderIds = $request->input('order_ids', []);
|
|
124
|
+
$vehicleIds = $request->input('vehicle_ids', []);
|
|
125
|
+
$driverIds = $request->input('driver_ids', []);
|
|
126
|
+
$options = $request->input('options', []);
|
|
127
|
+
// prior_assignments: assignments from previous phases that have not yet
|
|
128
|
+
// been committed to the database. Keyed by order_id (public_id).
|
|
129
|
+
$priorAssignments = collect($request->input('prior_assignments', []))
|
|
130
|
+
->keyBy('order_id');
|
|
131
|
+
|
|
132
|
+
// ── Resolve orders ────────────────────────────────────────────────────
|
|
133
|
+
$ordersQuery = Order::where('company_uuid', $companyUuid)
|
|
134
|
+
->whereIn('status', ['created', 'dispatched', 'started'])
|
|
135
|
+
->with(['payload.dropoff', 'payload.pickup', 'payload.waypoints', 'payload.waypointMarkers', 'payload.entities']);
|
|
136
|
+
|
|
137
|
+
if ($mode === 'assign_vehicles' || $mode === 'allocate') {
|
|
138
|
+
// Exclude orders that already have a vehicle assigned in the DB
|
|
139
|
+
// OR in a prior uncommitted phase.
|
|
140
|
+
$priorVehicleAssignedOrderIds = $priorAssignments
|
|
141
|
+
->filter(fn ($a) => !empty($a['vehicle_id']))
|
|
142
|
+
->keys()
|
|
143
|
+
->toArray();
|
|
144
|
+
$ordersQuery->whereNull('vehicle_assigned_uuid');
|
|
145
|
+
if (!empty($priorVehicleAssignedOrderIds)) {
|
|
146
|
+
$ordersQuery->whereNotIn('public_id', $priorVehicleAssignedOrderIds);
|
|
147
|
+
}
|
|
148
|
+
} elseif ($mode === 'optimize') {
|
|
149
|
+
$ordersQuery->whereNotNull('vehicle_assigned_uuid');
|
|
150
|
+
} elseif ($mode === 'optimize_routes') {
|
|
151
|
+
// optimize_routes: re-sequence stops for selected orders.
|
|
152
|
+
// No vehicle-assignment filter — the user picks the orders explicitly.
|
|
153
|
+
} elseif ($mode === 'assign_drivers') {
|
|
154
|
+
// For assign_drivers we need orders that have a vehicle assigned
|
|
155
|
+
// (either committed to DB or from a prior phase) but no driver yet.
|
|
156
|
+
$priorVehicleAssignedOrderIds = $priorAssignments
|
|
157
|
+
->filter(fn ($a) => !empty($a['vehicle_id']))
|
|
158
|
+
->keys()
|
|
159
|
+
->toArray();
|
|
160
|
+
|
|
161
|
+
if (!empty($priorVehicleAssignedOrderIds)) {
|
|
162
|
+
// Use the prior phase's vehicle assignments — fetch those orders
|
|
163
|
+
// regardless of their DB vehicle_assigned_uuid.
|
|
164
|
+
$ordersQuery->whereIn('public_id', $priorVehicleAssignedOrderIds)
|
|
165
|
+
->whereNull('driver_assigned_uuid');
|
|
166
|
+
} else {
|
|
167
|
+
// Standalone assign_drivers (no prior vehicle phase):
|
|
168
|
+
// Use all selected orders regardless of vehicle assignment.
|
|
169
|
+
// The engine will assign both a vehicle and a driver together.
|
|
170
|
+
$ordersQuery->whereNull('driver_assigned_uuid');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!empty($orderIds)) {
|
|
175
|
+
$ordersQuery->whereIn('public_id', $orderIds);
|
|
176
|
+
}
|
|
177
|
+
$orders = $ordersQuery->get();
|
|
178
|
+
|
|
179
|
+
// Augment orders with prior-phase vehicle AND driver assignments so the
|
|
180
|
+
// engines can group by vehicle_id and preserve driver_id even before the
|
|
181
|
+
// plan is committed to the database.
|
|
182
|
+
if ($priorAssignments->isNotEmpty()) {
|
|
183
|
+
// Pre-load all drivers referenced in prior assignments so we can
|
|
184
|
+
// attach them to vehicles without N+1 queries.
|
|
185
|
+
$priorDriverIds = $priorAssignments
|
|
186
|
+
->pluck('driver_id')
|
|
187
|
+
->filter()
|
|
188
|
+
->unique()
|
|
189
|
+
->values()
|
|
190
|
+
->toArray();
|
|
191
|
+
$priorDriverMap = collect();
|
|
192
|
+
if (!empty($priorDriverIds)) {
|
|
193
|
+
$priorDriverMap = Driver::whereIn('public_id', $priorDriverIds)
|
|
194
|
+
->get()
|
|
195
|
+
->keyBy('public_id');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
foreach ($orders as $order) {
|
|
199
|
+
$prior = $priorAssignments->get($order->public_id);
|
|
200
|
+
if (!$prior) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Temporarily set the vehicle_assigned_uuid on the model
|
|
205
|
+
// so engines that group by this field work correctly.
|
|
206
|
+
if (!empty($prior['vehicle_id']) && !$order->vehicle_assigned_uuid) {
|
|
207
|
+
// Resolve the Vehicle model and attach it
|
|
208
|
+
$vehicle = Vehicle::where('public_id', $prior['vehicle_id'])
|
|
209
|
+
->with(['driver' => fn ($q) => $q->with(['scheduleItems'])])
|
|
210
|
+
->first();
|
|
211
|
+
if ($vehicle) {
|
|
212
|
+
$order->vehicle_assigned_uuid = $vehicle->uuid;
|
|
213
|
+
|
|
214
|
+
// If the prior phase assigned a driver that is not yet
|
|
215
|
+
// linked to this vehicle in the DB, attach that driver
|
|
216
|
+
// to the vehicle relation so RouteSequencingEngine (and
|
|
217
|
+
// any other engine) can read $vehicle->driver correctly.
|
|
218
|
+
if (!empty($prior['driver_id'])) {
|
|
219
|
+
$priorDriver = $priorDriverMap->get($prior['driver_id']);
|
|
220
|
+
if ($priorDriver && (!$vehicle->driver || $vehicle->driver->public_id !== $prior['driver_id'])) {
|
|
221
|
+
$vehicle->setRelation('driver', $priorDriver);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
$order->setRelation('vehicle', $vehicle);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Also temporarily set driver_assigned_uuid so engines that
|
|
230
|
+
// check this field (e.g. for deduplication) see the prior assignment.
|
|
231
|
+
if (!empty($prior['driver_id']) && !$order->driver_assigned_uuid) {
|
|
232
|
+
$priorDriver = $priorDriverMap->get($prior['driver_id']);
|
|
233
|
+
if ($priorDriver) {
|
|
234
|
+
$order->driver_assigned_uuid = $priorDriver->uuid;
|
|
235
|
+
$order->setRelation('driverAssigned', $priorDriver);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Resolve vehicles ──────────────────────────────────────────────────
|
|
242
|
+
$vehiclesQuery = Vehicle::where('company_uuid', $companyUuid)
|
|
243
|
+
->with(['driver' => fn ($q) => $q->with(['scheduleItems'])]);
|
|
244
|
+
|
|
245
|
+
if (!empty($vehicleIds)) {
|
|
246
|
+
$vehiclesQuery->whereIn('public_id', $vehicleIds);
|
|
247
|
+
} elseif (!empty($driverIds)) {
|
|
248
|
+
$vehiclesQuery->whereHas('driver', fn ($q) => $q->whereIn('public_id', $driverIds));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// assign_vehicles, assign_drivers and optimize_routes do not require an
|
|
252
|
+
// online/assigned driver — use all matching vehicles as-is.
|
|
253
|
+
if (in_array($mode, ['assign_vehicles', 'assign_drivers', 'optimize_routes'])) {
|
|
254
|
+
$vehicles = $vehiclesQuery->get();
|
|
255
|
+
} else {
|
|
256
|
+
// Legacy allocate / optimize modes require a driver to be linked.
|
|
257
|
+
$vehicles = $vehiclesQuery->get()->filter(fn ($v) => $v->driver !== null);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if ($orders->isEmpty()) {
|
|
261
|
+
return response()->json([
|
|
262
|
+
'message' => 'No orders found for the given criteria.',
|
|
263
|
+
'assignments' => [],
|
|
264
|
+
'unassigned' => [],
|
|
265
|
+
], 200);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if ($vehicles->isEmpty() && $mode !== 'assign_drivers') {
|
|
269
|
+
return response()->json([
|
|
270
|
+
'message' => 'No available vehicles found.',
|
|
271
|
+
'assignments' => [],
|
|
272
|
+
'unassigned' => $orders->pluck('public_id'),
|
|
273
|
+
], 200);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── Run engine ────────────────────────────────────────────────────────
|
|
277
|
+
$engineId = $mode === 'assign_drivers'
|
|
278
|
+
? 'driver_assignment'
|
|
279
|
+
: ($request->input('options.engine') ?? Setting::lookup('fleetops.orchestrator_engine', 'greedy'));
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
if ($mode === 'assign_drivers') {
|
|
283
|
+
$engine = new DriverAssignmentEngine();
|
|
284
|
+
$result = $engine->assign($orders, $vehicles, $options);
|
|
285
|
+
} elseif ($mode === 'optimize_routes') {
|
|
286
|
+
// optimize_routes sequences stops within each vehicle's already-assigned
|
|
287
|
+
// order group — it does NOT re-assign orders to different vehicles.
|
|
288
|
+
//
|
|
289
|
+
// IMPORTANT: Do NOT call $orders->load(['vehicle', 'vehicle.driver']) here.
|
|
290
|
+
// The augmentation loop above already called setRelation('vehicle', $vehicle)
|
|
291
|
+
// with the prior-phase driver attached via setRelation('driver', $priorDriver).
|
|
292
|
+
// Calling ->load() would reload from the DB and OVERWRITE those in-memory
|
|
293
|
+
// relations, losing the uncommitted driver assignment from a prior phase.
|
|
294
|
+
//
|
|
295
|
+
// Instead, only load the vehicle relation for orders that have a
|
|
296
|
+
// vehicle_assigned_uuid in the DB but no in-memory relation set yet
|
|
297
|
+
// (i.e. standalone optimize_routes without a prior assign_drivers phase).
|
|
298
|
+
foreach ($orders as $order) {
|
|
299
|
+
if (!$order->relationLoaded('vehicle') && $order->vehicle_assigned_uuid) {
|
|
300
|
+
$vehicle = Vehicle::where('uuid', $order->vehicle_assigned_uuid)
|
|
301
|
+
->with(['driver'])
|
|
302
|
+
->first();
|
|
303
|
+
if ($vehicle) {
|
|
304
|
+
$order->setRelation('vehicle', $vehicle);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
$engine = new RouteSequencingEngine();
|
|
309
|
+
$result = $engine->sequence($orders, $options);
|
|
310
|
+
} else {
|
|
311
|
+
$engine = $this->registry->resolve($engineId);
|
|
312
|
+
$result = $engine->allocate($orders, $vehicles, $options);
|
|
313
|
+
}
|
|
314
|
+
} catch (\RuntimeException $e) {
|
|
315
|
+
// Engine is unavailable (e.g. VROOM not reachable).
|
|
316
|
+
// Return a structured JSON 503 so the frontend can display a
|
|
317
|
+
// user-friendly message instead of an unhandled exception page.
|
|
318
|
+
return response()->json([
|
|
319
|
+
'error' => $e->getMessage(),
|
|
320
|
+
'hint' => 'If you are using the VROOM engine, ensure the VROOM service is running and VROOM_HOST is configured correctly. Alternatively, switch to the built-in "greedy" engine in Orchestrator Settings.',
|
|
321
|
+
'engine' => $engineId,
|
|
322
|
+
], 503);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return response()->json($result);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Preview an orchestration run without committing any assignments.
|
|
330
|
+
*
|
|
331
|
+
* GET /int/v1/fleet-ops/orchestrator/preview
|
|
332
|
+
*/
|
|
333
|
+
public function preview(Request $request): JsonResponse
|
|
334
|
+
{
|
|
335
|
+
return $this->run($request);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Commit an orchestration plan — creates Manifests and ManifestStops.
|
|
340
|
+
*
|
|
341
|
+
* Does NOT trigger dispatch or update order status. That is the
|
|
342
|
+
* responsibility of the operational flow (driver actions / dispatcher).
|
|
343
|
+
*
|
|
344
|
+
* POST /int/v1/fleet-ops/orchestrator/commit
|
|
345
|
+
*/
|
|
346
|
+
public function commit(Request $request): JsonResponse
|
|
347
|
+
{
|
|
348
|
+
$assignments = $request->input('assignments', []);
|
|
349
|
+
$scheduledDate = $request->input('scheduled_date', now()->toDateString());
|
|
350
|
+
$companyUuid = session('company');
|
|
351
|
+
|
|
352
|
+
if (empty($assignments)) {
|
|
353
|
+
return response()->json(['error' => 'No assignments provided.'], 422);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
$committed = [];
|
|
357
|
+
$failed = [];
|
|
358
|
+
$manifests = [];
|
|
359
|
+
|
|
360
|
+
DB::beginTransaction();
|
|
361
|
+
try {
|
|
362
|
+
// Group assignments by vehicle_id
|
|
363
|
+
$byVehicle = [];
|
|
364
|
+
foreach ($assignments as $assignment) {
|
|
365
|
+
$vehicleId = $assignment['vehicle_id'] ?? null;
|
|
366
|
+
if (!$vehicleId) {
|
|
367
|
+
$failed[] = $assignment['order_id'] ?? 'unknown';
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
$byVehicle[$vehicleId][] = $assignment;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
foreach ($byVehicle as $vehiclePublicId => $vehicleAssignments) {
|
|
374
|
+
$vehicle = Vehicle::where('public_id', $vehiclePublicId)->first();
|
|
375
|
+
if (!$vehicle) {
|
|
376
|
+
foreach ($vehicleAssignments as $a) {
|
|
377
|
+
$failed[] = $a['order_id'];
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Driver is optional (vehicle-only assignment)
|
|
383
|
+
$driverPublicId = $vehicleAssignments[0]['driver_id'] ?? null;
|
|
384
|
+
$driver = $driverPublicId
|
|
385
|
+
? Driver::where('public_id', $driverPublicId)->first()
|
|
386
|
+
: null;
|
|
387
|
+
|
|
388
|
+
$totalDistance = (int) array_sum(array_column($vehicleAssignments, 'distance'));
|
|
389
|
+
$totalDuration = (int) array_sum(array_column($vehicleAssignments, 'duration'));
|
|
390
|
+
|
|
391
|
+
// Create Manifest
|
|
392
|
+
$manifest = Manifest::create([
|
|
393
|
+
'company_uuid' => $companyUuid,
|
|
394
|
+
'vehicle_uuid' => $vehicle->uuid,
|
|
395
|
+
'driver_uuid' => $driver?->uuid,
|
|
396
|
+
'status' => 'draft',
|
|
397
|
+
'scheduled_date' => $scheduledDate,
|
|
398
|
+
'total_distance_m' => $totalDistance,
|
|
399
|
+
'total_duration_s' => $totalDuration,
|
|
400
|
+
'stop_count' => count($vehicleAssignments),
|
|
401
|
+
]);
|
|
402
|
+
|
|
403
|
+
// Sort stops by sequence
|
|
404
|
+
usort($vehicleAssignments, fn ($a, $b) => ($a['sequence'] ?? 0) <=> ($b['sequence'] ?? 0));
|
|
405
|
+
|
|
406
|
+
foreach ($vehicleAssignments as $idx => $assignment) {
|
|
407
|
+
$order = Order::where('public_id', $assignment['order_id'])->first();
|
|
408
|
+
if (!$order) {
|
|
409
|
+
$failed[] = $assignment['order_id'];
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
$placeUuid = $order->payload?->dropoff?->uuid ?? null;
|
|
414
|
+
|
|
415
|
+
ManifestStop::create([
|
|
416
|
+
'manifest_uuid' => $manifest->uuid,
|
|
417
|
+
'order_uuid' => $order->uuid,
|
|
418
|
+
'place_uuid' => $placeUuid,
|
|
419
|
+
'sequence' => (int) ($assignment['sequence'] ?? ($idx + 1)),
|
|
420
|
+
'status' => 'pending',
|
|
421
|
+
'estimated_arrival' => isset($assignment['arrival'])
|
|
422
|
+
? Carbon::createFromTimestamp($assignment['arrival'])
|
|
423
|
+
: null,
|
|
424
|
+
'distance_from_prev_m' => (int) ($assignment['distance'] ?? 0),
|
|
425
|
+
'duration_from_prev_s' => (int) ($assignment['duration'] ?? 0),
|
|
426
|
+
]);
|
|
427
|
+
|
|
428
|
+
// Update order assignments
|
|
429
|
+
$order->vehicle_assigned_uuid = $vehicle->uuid;
|
|
430
|
+
$order->manifest_uuid = $manifest->uuid;
|
|
431
|
+
if ($driver) {
|
|
432
|
+
$order->driver_assigned_uuid = $driver->uuid;
|
|
433
|
+
}
|
|
434
|
+
if (isset($assignment['sequence'])) {
|
|
435
|
+
$order->is_route_optimized = true;
|
|
436
|
+
}
|
|
437
|
+
$order->save();
|
|
438
|
+
|
|
439
|
+
// Update waypoint sequence if provided
|
|
440
|
+
if (!empty($assignment['waypoint_sequence']) && $order->payload) {
|
|
441
|
+
foreach ($assignment['waypoint_sequence'] as $seq => $waypointId) {
|
|
442
|
+
DB::table('waypoints')
|
|
443
|
+
->where('payload_uuid', $order->payload_uuid)
|
|
444
|
+
->where('public_id', $waypointId)
|
|
445
|
+
->update(['order' => $seq]);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
$committed[] = $assignment['order_id'];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
$manifests[] = $manifest->public_id;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
DB::commit();
|
|
456
|
+
} catch (\Exception $e) {
|
|
457
|
+
// Only roll back if a transaction is still active.
|
|
458
|
+
// A PDOException from a missing table can cause MySQL to implicitly
|
|
459
|
+
// roll back the transaction before we reach this catch block.
|
|
460
|
+
if (DB::transactionLevel() > 0) {
|
|
461
|
+
DB::rollBack();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return response()->json(['error' => 'Commit failed: ' . $e->getMessage()], 500);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return response()->json([
|
|
468
|
+
'committed' => $committed,
|
|
469
|
+
'failed' => $failed,
|
|
470
|
+
'manifests' => $manifests,
|
|
471
|
+
]);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Return available orchestration engines.
|
|
476
|
+
*
|
|
477
|
+
* GET /int/v1/fleet-ops/orchestrator/engines
|
|
478
|
+
*/
|
|
479
|
+
public function engines(): JsonResponse
|
|
480
|
+
{
|
|
481
|
+
return response()->json([
|
|
482
|
+
'engines' => $this->registry->available(),
|
|
483
|
+
]);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Return all active Order Configs with their custom field definitions.
|
|
488
|
+
* Used by the Orchestrator Settings UI for configurable card fields.
|
|
489
|
+
*
|
|
490
|
+
* GET /int/v1/fleet-ops/orchestrator/order-config-fields
|
|
491
|
+
*/
|
|
492
|
+
public function orderConfigFields(): JsonResponse
|
|
493
|
+
{
|
|
494
|
+
$companyUuid = session('company');
|
|
495
|
+
|
|
496
|
+
$configs = OrderConfig::where('company_uuid', $companyUuid)
|
|
497
|
+
->with('customFields')
|
|
498
|
+
->get(['uuid', 'public_id', 'name', 'key'])
|
|
499
|
+
->map(function ($config) {
|
|
500
|
+
// customFields is a morphMany on subject_uuid/subject_type.
|
|
501
|
+
// If the eager load returned nothing (e.g. subject_type mismatch),
|
|
502
|
+
// fall back to a direct query by subject_uuid.
|
|
503
|
+
$customFields = $config->customFields;
|
|
504
|
+
if ($customFields->isEmpty()) {
|
|
505
|
+
$customFields = \Fleetbase\Models\CustomField::where('subject_uuid', $config->uuid)
|
|
506
|
+
->orderBy('order')
|
|
507
|
+
->get();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
$fields = $customFields
|
|
511
|
+
->map(fn ($field) => [
|
|
512
|
+
'key' => $field->name ?? Str::slug($field->label ?? '', '_'),
|
|
513
|
+
'label' => $field->label ?? $field->name ?? '',
|
|
514
|
+
'type' => $field->type ?? 'text',
|
|
515
|
+
'required' => (bool) ($field->required ?? false),
|
|
516
|
+
])
|
|
517
|
+
->values();
|
|
518
|
+
|
|
519
|
+
return [
|
|
520
|
+
'id' => $config->public_id,
|
|
521
|
+
'uuid' => $config->uuid,
|
|
522
|
+
'name' => $config->name,
|
|
523
|
+
'key' => $config->key,
|
|
524
|
+
'fields' => $fields,
|
|
525
|
+
];
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Exclude configs that have no custom fields at all
|
|
529
|
+
$configs = $configs->filter(fn ($config) => count($config['fields']) > 0)->values();
|
|
530
|
+
|
|
531
|
+
return response()->json(['configs' => $configs]);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Import orders from parsed CSV/Excel row data.
|
|
536
|
+
*
|
|
537
|
+
* POST /int/v1/fleet-ops/orchestrator/import-orders
|
|
538
|
+
*/
|
|
539
|
+
public function importOrders(Request $request): JsonResponse
|
|
540
|
+
{
|
|
541
|
+
$rows = $request->input('rows', []);
|
|
542
|
+
$companyUuid = session('company');
|
|
543
|
+
|
|
544
|
+
if (empty($rows)) {
|
|
545
|
+
return response()->json(['error' => 'No rows provided.'], 422);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
$created = [];
|
|
549
|
+
$failed = [];
|
|
550
|
+
|
|
551
|
+
// ── Group multi-waypoint rows by order_ref ────────────────────────────
|
|
552
|
+
// Rows with order_type = 'multi_waypoint' and the same order_ref are
|
|
553
|
+
// collapsed into a single order where each row becomes one waypoint.
|
|
554
|
+
$groups = [];
|
|
555
|
+
foreach ($rows as $row) {
|
|
556
|
+
$orderType = strtolower(trim($row['order_type'] ?? 'pickup_dropoff'));
|
|
557
|
+
$orderRef = trim($row['order_ref'] ?? '');
|
|
558
|
+
|
|
559
|
+
if ($orderType === 'multi_waypoint' && $orderRef !== '') {
|
|
560
|
+
$groups[$orderRef][] = $row;
|
|
561
|
+
} else {
|
|
562
|
+
// Each pickup/dropoff row is its own independent group.
|
|
563
|
+
$groups['__single_' . Str::uuid()][] = $row;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
foreach ($groups as $groupKey => $groupRows) {
|
|
568
|
+
DB::beginTransaction();
|
|
569
|
+
try {
|
|
570
|
+
// Use the first row for order-level metadata.
|
|
571
|
+
$firstRow = $groupRows[0];
|
|
572
|
+
$orderType = strtolower(trim($firstRow['order_type'] ?? 'pickup_dropoff'));
|
|
573
|
+
$isMulti = $orderType === 'multi_waypoint';
|
|
574
|
+
|
|
575
|
+
// ── Resolve OrderConfig ───────────────────────────────────────
|
|
576
|
+
$orderConfigUuid = null;
|
|
577
|
+
if (!empty($firstRow['type'])) {
|
|
578
|
+
$orderConfig = OrderConfig::resolveFromIdentifier($firstRow['type']);
|
|
579
|
+
if ($orderConfig) {
|
|
580
|
+
$orderConfigUuid = $orderConfig->uuid;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ── Resolve Customer ─────────────────────────────────────────
|
|
585
|
+
$customerUuid = null;
|
|
586
|
+
$customerType = null;
|
|
587
|
+
if (!empty($firstRow['customer_email']) || !empty($firstRow['customer_phone']) || !empty($firstRow['customer_name'])) {
|
|
588
|
+
$customerEntityType = strtolower(trim($firstRow['customer_type'] ?? 'contact'));
|
|
589
|
+
if ($customerEntityType === 'vendor') {
|
|
590
|
+
$vendor = $this->resolveOrCreateVendor($firstRow, $companyUuid, 'customer');
|
|
591
|
+
if ($vendor) {
|
|
592
|
+
$customerUuid = $vendor->uuid;
|
|
593
|
+
$customerType = 'Fleetbase\\FleetOps\\Models\\Vendor';
|
|
594
|
+
}
|
|
595
|
+
} else {
|
|
596
|
+
$contact = $this->resolveOrCreateContact($firstRow, $companyUuid, 'customer');
|
|
597
|
+
if ($contact) {
|
|
598
|
+
$customerUuid = $contact->uuid;
|
|
599
|
+
$customerType = 'Fleetbase\\FleetOps\\Models\\Contact';
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ── Resolve Facilitator ───────────────────────────────────────
|
|
605
|
+
$facilitatorUuid = null;
|
|
606
|
+
$facilitatorType = null;
|
|
607
|
+
if (!empty($firstRow['facilitator_email']) || !empty($firstRow['facilitator_phone']) || !empty($firstRow['facilitator_name'])) {
|
|
608
|
+
$facilitatorEntityType = strtolower(trim($firstRow['facilitator_type'] ?? 'vendor'));
|
|
609
|
+
if ($facilitatorEntityType === 'contact') {
|
|
610
|
+
$contact = $this->resolveOrCreateContact($firstRow, $companyUuid, 'facilitator');
|
|
611
|
+
if ($contact) {
|
|
612
|
+
$facilitatorUuid = $contact->uuid;
|
|
613
|
+
$facilitatorType = 'Fleetbase\\FleetOps\\Models\\Contact';
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
$vendor = $this->resolveOrCreateVendor($firstRow, $companyUuid, 'facilitator');
|
|
617
|
+
if ($vendor) {
|
|
618
|
+
$facilitatorUuid = $vendor->uuid;
|
|
619
|
+
$facilitatorType = 'Fleetbase\\FleetOps\\Models\\Vendor';
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ── Resolve Vehicle ───────────────────────────────────────────
|
|
625
|
+
$vehicleUuid = null;
|
|
626
|
+
if (!empty($firstRow['vehicle_plate'])) {
|
|
627
|
+
$vehicle = Vehicle::where('company_uuid', $companyUuid)
|
|
628
|
+
->where('plate_number', $firstRow['vehicle_plate'])
|
|
629
|
+
->first();
|
|
630
|
+
if ($vehicle) {
|
|
631
|
+
$vehicleUuid = $vehicle->uuid;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ── Resolve Driver ────────────────────────────────────────────
|
|
636
|
+
$driverUuid = null;
|
|
637
|
+
$driverIdentifier = $firstRow['driver_email'] ?? $firstRow['driver_phone'] ?? $firstRow['driver_name'] ?? null;
|
|
638
|
+
if ($driverIdentifier) {
|
|
639
|
+
$driver = Driver::findByIdentifier($driverIdentifier);
|
|
640
|
+
if ($driver) {
|
|
641
|
+
$driverUuid = $driver->uuid;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ── Build required_skills array ───────────────────────────────
|
|
646
|
+
$requiredSkills = [];
|
|
647
|
+
if (!empty($firstRow['required_skills'])) {
|
|
648
|
+
$requiredSkills = array_filter(array_map('trim', explode(',', $firstRow['required_skills'])));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ── Create the Order ─────────────────────────────────────────
|
|
652
|
+
$order = Order::create([
|
|
653
|
+
'company_uuid' => $companyUuid,
|
|
654
|
+
'order_config_uuid' => $orderConfigUuid,
|
|
655
|
+
'customer_uuid' => $customerUuid,
|
|
656
|
+
'customer_type' => $customerType,
|
|
657
|
+
'facilitator_uuid' => $facilitatorUuid,
|
|
658
|
+
'facilitator_type' => $facilitatorType,
|
|
659
|
+
'vehicle_assigned_uuid' => $vehicleUuid,
|
|
660
|
+
'driver_assigned_uuid' => $driverUuid,
|
|
661
|
+
'internal_id' => $firstRow['internal_id'] ?? null,
|
|
662
|
+
'status' => $firstRow['status'] ?? 'created',
|
|
663
|
+
'type' => $firstRow['type'] ?? 'default',
|
|
664
|
+
'notes' => $firstRow['notes'] ?? null,
|
|
665
|
+
'scheduled_at' => !empty($firstRow['scheduled_at'])
|
|
666
|
+
? Carbon::parse($firstRow['scheduled_at'])
|
|
667
|
+
: null,
|
|
668
|
+
'time_window_start' => $firstRow['time_window_start'] ?? null,
|
|
669
|
+
'time_window_end' => $firstRow['time_window_end'] ?? null,
|
|
670
|
+
'required_skills' => $requiredSkills,
|
|
671
|
+
'orchestrator_priority' => (int) ($firstRow['priority'] ?? 0),
|
|
672
|
+
'meta' => array_filter([
|
|
673
|
+
'service_time_min' => $firstRow['service_time_min'] ?? null,
|
|
674
|
+
'order_type' => $orderType,
|
|
675
|
+
'order_ref' => $firstRow['order_ref'] ?? null,
|
|
676
|
+
]),
|
|
677
|
+
]);
|
|
678
|
+
|
|
679
|
+
// ── Create Payload ────────────────────────────────────────────
|
|
680
|
+
$payload = Payload::create([
|
|
681
|
+
'company_uuid' => $companyUuid,
|
|
682
|
+
'cod_amount' => $firstRow['cod_amount'] ?? null,
|
|
683
|
+
'cod_currency' => $firstRow['cod_currency'] ?? null,
|
|
684
|
+
'capacity_weight_kg' => $firstRow['weight_kg'] ?? null,
|
|
685
|
+
'capacity_volume_m3' => $firstRow['volume_m3'] ?? null,
|
|
686
|
+
'capacity_parcels' => $firstRow['parcels'] ?? null,
|
|
687
|
+
]);
|
|
688
|
+
|
|
689
|
+
$order->setPayload($payload);
|
|
690
|
+
$order->save();
|
|
691
|
+
|
|
692
|
+
// ── Attach Places ────────────────────────────────────────────────
|
|
693
|
+
$entities = [];
|
|
694
|
+
if ($isMulti) {
|
|
695
|
+
// ── Multi-waypoint ───────────────────────────────────────
|
|
696
|
+
// Each row with an address becomes a waypoint stop, tagged
|
|
697
|
+
// with a stable _import_id ('wp_0', 'wp_1', …).
|
|
698
|
+
//
|
|
699
|
+
// Multiple entities per order are supported: any row in the
|
|
700
|
+
// group that has entity fields will produce an entity. If
|
|
701
|
+
// the row also has an address it is linked to that waypoint
|
|
702
|
+
// via _import_id; otherwise entity_destination is used to
|
|
703
|
+
// resolve the target stop (index, 'pickup', or 'dropoff').
|
|
704
|
+
$waypoints = [];
|
|
705
|
+
$wpImportIds = []; // rowIndex => _import_id for address rows
|
|
706
|
+
|
|
707
|
+
foreach ($groupRows as $wpIndex => $wpRow) {
|
|
708
|
+
$importId = 'wp_' . $wpIndex;
|
|
709
|
+
$placeData = $this->buildPlaceData($wpRow, 'dropoff');
|
|
710
|
+
if (!empty($placeData['street1'])) {
|
|
711
|
+
$place = Place::createFromMixed($placeData, [], true);
|
|
712
|
+
if ($place) {
|
|
713
|
+
$waypoints[] = [
|
|
714
|
+
'place_uuid' => $place->uuid,
|
|
715
|
+
'type' => 'dropoff',
|
|
716
|
+
'_import_id' => $importId,
|
|
717
|
+
];
|
|
718
|
+
$wpImportIds[$wpIndex] = $importId;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (!empty($waypoints)) {
|
|
724
|
+
$payload->setWaypoints($waypoints);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Collect entities from ALL rows in the group
|
|
728
|
+
foreach ($groupRows as $wpIndex => $wpRow) {
|
|
729
|
+
$entityData = $this->buildEntityData($wpRow, $companyUuid);
|
|
730
|
+
if ($entityData === null) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Prefer the _import_id of this row's own waypoint;
|
|
735
|
+
// fall back to entity_destination if the row has no address.
|
|
736
|
+
if (isset($wpImportIds[$wpIndex])) {
|
|
737
|
+
$entityData['_import_id'] = $wpImportIds[$wpIndex];
|
|
738
|
+
} else {
|
|
739
|
+
$dest = $wpRow['entity_destination'] ?? null;
|
|
740
|
+
if ($dest !== null && $dest !== '') {
|
|
741
|
+
// Numeric string → waypoint index; otherwise 'pickup'/'dropoff'
|
|
742
|
+
if (is_numeric($dest)) {
|
|
743
|
+
$targetIndex = (int) $dest;
|
|
744
|
+
if (isset($wpImportIds[$targetIndex])) {
|
|
745
|
+
$entityData['_import_id'] = $wpImportIds[$targetIndex];
|
|
746
|
+
}
|
|
747
|
+
} else {
|
|
748
|
+
$entityData['destination'] = $dest;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
$entities[] = $entityData;
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
// ── Pickup / Dropoff ─────────────────────────────────────
|
|
757
|
+
// Address fields come from the first row only.
|
|
758
|
+
// Entity rows: ALL rows in the group are scanned so that
|
|
759
|
+
// multiple entities (one per row) can be attached to the
|
|
760
|
+
// same order. Only the first row is used for addresses.
|
|
761
|
+
$pickupData = $this->buildPlaceData($firstRow, 'pickup');
|
|
762
|
+
$dropoffData = $this->buildPlaceData($firstRow, 'dropoff');
|
|
763
|
+
|
|
764
|
+
if (!empty($pickupData['street1'])) {
|
|
765
|
+
$payload->setPickup($pickupData, ['save' => true]);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (!empty($dropoffData['street1'])) {
|
|
769
|
+
$payload->setDropoff($dropoffData, ['save' => true]);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Collect entities from ALL rows in the group
|
|
773
|
+
foreach ($groupRows as $entityRow) {
|
|
774
|
+
$entityData = $this->buildEntityData($entityRow, $companyUuid);
|
|
775
|
+
if ($entityData === null) {
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
// Resolve destination from each row's entity_destination
|
|
779
|
+
// column; default to 'dropoff' when not set.
|
|
780
|
+
$dest = $entityRow['entity_destination'] ?? 'dropoff';
|
|
781
|
+
$entityData['destination'] = ($dest !== '') ? $dest : 'dropoff';
|
|
782
|
+
$entities[] = $entityData;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// ── Attach Entities to Payload ──────────────────────────────
|
|
787
|
+
if (!empty($entities)) {
|
|
788
|
+
$payload->load(['waypoints', 'pickup', 'dropoff']);
|
|
789
|
+
$payload->setEntities($entities);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
DB::commit();
|
|
793
|
+
$created[] = $order->public_id;
|
|
794
|
+
} catch (\Exception $e) {
|
|
795
|
+
DB::rollBack();
|
|
796
|
+
$rowIndex = ($groupRows[0]['_rowIndex'] ?? '?');
|
|
797
|
+
$failed[] = ['row' => $rowIndex, 'error' => $e->getMessage()];
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return response()->json([
|
|
802
|
+
'created' => $created,
|
|
803
|
+
'failed' => $failed,
|
|
804
|
+
]);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// ── Private helpers ───────────────────────────────────────────────────────
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Build a Place-compatible attributes array from a CSV row.
|
|
811
|
+
*
|
|
812
|
+
* @param array $row the mapped CSV row
|
|
813
|
+
* @param string $prefix 'pickup' or 'dropoff'
|
|
814
|
+
*/
|
|
815
|
+
private function buildPlaceData(array $row, string $prefix): array
|
|
816
|
+
{
|
|
817
|
+
return array_filter([
|
|
818
|
+
'name' => $row["{$prefix}_name"] ?? null,
|
|
819
|
+
'street1' => $row["{$prefix}_street1"] ?? null,
|
|
820
|
+
'street2' => $row["{$prefix}_street2"] ?? null,
|
|
821
|
+
'city' => $row["{$prefix}_city"] ?? null,
|
|
822
|
+
'province' => $row["{$prefix}_state"] ?? null,
|
|
823
|
+
'postal_code' => $row["{$prefix}_postal_code"] ?? null,
|
|
824
|
+
'country' => $row["{$prefix}_country"] ?? null,
|
|
825
|
+
'phone' => $row["{$prefix}_phone"] ?? null,
|
|
826
|
+
'location' => $this->buildLocationPoint($row["{$prefix}_lat"] ?? null, $row["{$prefix}_lng"] ?? null),
|
|
827
|
+
]);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Build a WKT POINT string from optional lat/lng strings.
|
|
832
|
+
* Returns null when either value is missing or both are zero.
|
|
833
|
+
*/
|
|
834
|
+
private function buildLocationPoint(?string $lat, ?string $lng): ?string
|
|
835
|
+
{
|
|
836
|
+
if (empty($lat) || empty($lng)) {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
$latF = (float) $lat;
|
|
840
|
+
$lngF = (float) $lng;
|
|
841
|
+
if ($latF === 0.0 && $lngF === 0.0) {
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return "POINT({$lngF} {$latF})";
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Build an Entity-compatible attributes array from a CSV row.
|
|
850
|
+
* Returns null when no entity fields are present in the row.
|
|
851
|
+
*
|
|
852
|
+
* @param array $row the mapped CSV row
|
|
853
|
+
* @param string $companyUuid the current company UUID
|
|
854
|
+
*/
|
|
855
|
+
private function buildEntityData(array $row, string $companyUuid): ?array
|
|
856
|
+
{
|
|
857
|
+
// Only build an entity if at least one entity field has a value
|
|
858
|
+
$hasEntity = !empty($row['entity_name'])
|
|
859
|
+
|| !empty($row['entity_type'])
|
|
860
|
+
|| !empty($row['entity_sku'])
|
|
861
|
+
|| !empty($row['entity_barcode'])
|
|
862
|
+
|| !empty($row['entity_description']);
|
|
863
|
+
|
|
864
|
+
if (!$hasEntity) {
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return array_filter([
|
|
869
|
+
'company_uuid' => $companyUuid,
|
|
870
|
+
'name' => $row['entity_name'] ?? null,
|
|
871
|
+
'type' => $row['entity_type'] ?? null,
|
|
872
|
+
'description' => $row['entity_description'] ?? null,
|
|
873
|
+
'sku' => $row['entity_sku'] ?? null,
|
|
874
|
+
'barcode' => $row['entity_barcode'] ?? null,
|
|
875
|
+
'internal_id' => $row['entity_internal_id'] ?? null,
|
|
876
|
+
'declared_value' => isset($row['entity_declared_value']) && $row['entity_declared_value'] !== ''
|
|
877
|
+
? (float) $row['entity_declared_value'] : null,
|
|
878
|
+
'currency' => $row['entity_currency'] ?? null,
|
|
879
|
+
'price' => isset($row['entity_price']) && $row['entity_price'] !== ''
|
|
880
|
+
? (float) $row['entity_price'] : null,
|
|
881
|
+
'sale_price' => isset($row['entity_sale_price']) && $row['entity_sale_price'] !== ''
|
|
882
|
+
? (float) $row['entity_sale_price'] : null,
|
|
883
|
+
'weight' => isset($row['entity_weight']) && $row['entity_weight'] !== ''
|
|
884
|
+
? (float) $row['entity_weight'] : null,
|
|
885
|
+
'weight_unit' => $row['entity_weight_unit'] ?? null,
|
|
886
|
+
'length' => isset($row['entity_length']) && $row['entity_length'] !== ''
|
|
887
|
+
? (float) $row['entity_length'] : null,
|
|
888
|
+
'width' => isset($row['entity_width']) && $row['entity_width'] !== ''
|
|
889
|
+
? (float) $row['entity_width'] : null,
|
|
890
|
+
'height' => isset($row['entity_height']) && $row['entity_height'] !== ''
|
|
891
|
+
? (float) $row['entity_height'] : null,
|
|
892
|
+
'dimensions_unit' => $row['entity_dimensions_unit'] ?? null,
|
|
893
|
+
], fn ($v) => $v !== null);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Resolve an existing Contact by email/phone, or create a new one.
|
|
898
|
+
*
|
|
899
|
+
* @param array $row the mapped CSV row
|
|
900
|
+
* @param string $companyUuid the company UUID
|
|
901
|
+
* @param string $prefix 'customer' or 'facilitator'
|
|
902
|
+
*/
|
|
903
|
+
private function resolveOrCreateContact(array $row, string $companyUuid, string $prefix): ?Contact
|
|
904
|
+
{
|
|
905
|
+
$email = $row["{$prefix}_email"] ?? null;
|
|
906
|
+
$phone = $row["{$prefix}_phone"] ?? null;
|
|
907
|
+
$name = $row["{$prefix}_name"] ?? null;
|
|
908
|
+
|
|
909
|
+
// Try to find by email first, then phone.
|
|
910
|
+
$contact = null;
|
|
911
|
+
if ($email) {
|
|
912
|
+
$contact = Contact::where('company_uuid', $companyUuid)->where('email', $email)->first();
|
|
913
|
+
}
|
|
914
|
+
if (!$contact && $phone) {
|
|
915
|
+
$contact = Contact::where('company_uuid', $companyUuid)->where('phone', $phone)->first();
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if ($contact) {
|
|
919
|
+
return $contact;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Create a new contact if we have at least a name or email.
|
|
923
|
+
if ($name || $email) {
|
|
924
|
+
return Contact::create(array_filter([
|
|
925
|
+
'company_uuid' => $companyUuid,
|
|
926
|
+
'name' => $name,
|
|
927
|
+
'email' => $email,
|
|
928
|
+
'phone' => $phone,
|
|
929
|
+
'type' => 'customer',
|
|
930
|
+
]));
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Resolve an existing Vendor by email/phone, or create a new one.
|
|
938
|
+
*
|
|
939
|
+
* @param array $row the mapped CSV row
|
|
940
|
+
* @param string $companyUuid the company UUID
|
|
941
|
+
* @param string $prefix 'customer' or 'facilitator'
|
|
942
|
+
*/
|
|
943
|
+
private function resolveOrCreateVendor(array $row, string $companyUuid, string $prefix): ?Vendor
|
|
944
|
+
{
|
|
945
|
+
$email = $row["{$prefix}_email"] ?? null;
|
|
946
|
+
$phone = $row["{$prefix}_phone"] ?? null;
|
|
947
|
+
$name = $row["{$prefix}_name"] ?? null;
|
|
948
|
+
|
|
949
|
+
$vendor = null;
|
|
950
|
+
if ($email) {
|
|
951
|
+
$vendor = Vendor::where('company_uuid', $companyUuid)->where('email', $email)->first();
|
|
952
|
+
}
|
|
953
|
+
if (!$vendor && $phone) {
|
|
954
|
+
$vendor = Vendor::where('company_uuid', $companyUuid)->where('phone', $phone)->first();
|
|
955
|
+
}
|
|
956
|
+
if (!$vendor && $name) {
|
|
957
|
+
$vendor = Vendor::where('company_uuid', $companyUuid)->whereRaw('lower(name) = ?', [strtolower($name)])->first();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if ($vendor) {
|
|
961
|
+
return $vendor;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if ($name || $email) {
|
|
965
|
+
return Vendor::create(array_filter([
|
|
966
|
+
'company_uuid' => $companyUuid,
|
|
967
|
+
'name' => $name,
|
|
968
|
+
'email' => $email,
|
|
969
|
+
'phone' => $phone,
|
|
970
|
+
]));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
}
|