@fleetbase/fleetops-engine 0.6.38 → 0.6.40
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 +43 -38
- package/addon/components/layout/fleet-ops-sidebar.js +103 -24
- 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 +1088 -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 +52 -0
- package/addon/components/vehicle/form.hbs +51 -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 +79 -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 +4 -2
- 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 +53 -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 +38 -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/migrations/2026_04_14_000001_drop_redundant_capacity_columns_from_payloads_table.php +42 -0
- package/server/migrations/2026_04_14_000002_rename_capacity_columns_on_vehicles_table.php +48 -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/Api/v1/OrderController.php +15 -2
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +18 -2
- 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/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 +18 -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 +380 -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,807 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { inject as service } from '@ember/service';
|
|
3
|
+
import { tracked } from '@glimmer/tracking';
|
|
4
|
+
import { action } from '@ember/object';
|
|
5
|
+
import { task } from 'ember-concurrency';
|
|
6
|
+
import * as XLSX from 'xlsx';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Orchestrator Import Modal
|
|
10
|
+
*
|
|
11
|
+
* Three-step import flow:
|
|
12
|
+
* 1. Upload — drag-and-drop or browse for CSV / Excel file
|
|
13
|
+
* 2. Map — map file columns to Fleetbase order fields (grouped by section)
|
|
14
|
+
* 3. Preview — validate rows, review errors, confirm import
|
|
15
|
+
*
|
|
16
|
+
* The import template supports two order types:
|
|
17
|
+
* - pickup_dropoff (default) — a single pickup and a single dropoff address
|
|
18
|
+
* - multi_waypoint — multiple waypoints identified by a shared order_ref
|
|
19
|
+
*
|
|
20
|
+
* Entity resolution (customer, facilitator, vehicle, driver) is performed
|
|
21
|
+
* server-side: existing records are found by email/phone/plate before creating new ones.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// ── Field sections ─────────────────────────────────────────────────────────────
|
|
25
|
+
const FIELD_SECTIONS = [
|
|
26
|
+
{
|
|
27
|
+
key: 'order',
|
|
28
|
+
label: 'Order Details',
|
|
29
|
+
fields: [
|
|
30
|
+
{ key: 'order_type', label: 'Order Type', required: false, hint: 'pickup_dropoff or multi_waypoint' },
|
|
31
|
+
{ key: 'order_ref', label: 'Order Reference', required: false, hint: 'Groups rows into one multi-waypoint order' },
|
|
32
|
+
{ key: 'internal_id', label: 'Internal ID', required: false },
|
|
33
|
+
{ key: 'type', label: 'Order Config Slug', required: false, hint: 'e.g. default, delivery, transfer' },
|
|
34
|
+
{ key: 'status', label: 'Status', required: false },
|
|
35
|
+
{ key: 'scheduled_at', label: 'Scheduled At', required: false, hint: 'YYYY-MM-DD HH:mm' },
|
|
36
|
+
{ key: 'notes', label: 'Notes', required: false },
|
|
37
|
+
{ key: 'priority', label: 'Priority (0-100)', required: false },
|
|
38
|
+
{ key: 'time_window_start', label: 'Time Window Start', required: false, hint: 'HH:mm' },
|
|
39
|
+
{ key: 'time_window_end', label: 'Time Window End', required: false, hint: 'HH:mm' },
|
|
40
|
+
{ key: 'service_time_min', label: 'Service Time (min)', required: false },
|
|
41
|
+
{ key: 'required_skills', label: 'Required Skills', required: false, hint: 'Comma-separated' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: 'pickup',
|
|
46
|
+
label: 'Pickup / Origin',
|
|
47
|
+
fields: [
|
|
48
|
+
{ key: 'pickup_name', label: 'Pickup Name', required: false },
|
|
49
|
+
{ key: 'pickup_street1', label: 'Pickup Street 1', required: false },
|
|
50
|
+
{ key: 'pickup_street2', label: 'Pickup Street 2', required: false },
|
|
51
|
+
{ key: 'pickup_city', label: 'Pickup City', required: false },
|
|
52
|
+
{ key: 'pickup_state', label: 'Pickup State', required: false },
|
|
53
|
+
{ key: 'pickup_postal_code', label: 'Pickup Postal Code', required: false },
|
|
54
|
+
{ key: 'pickup_country', label: 'Pickup Country', required: false, hint: 'ISO 3166-1 alpha-2' },
|
|
55
|
+
{ key: 'pickup_phone', label: 'Pickup Phone', required: false },
|
|
56
|
+
{ key: 'pickup_lat', label: 'Pickup Latitude', required: false },
|
|
57
|
+
{ key: 'pickup_lng', label: 'Pickup Longitude', required: false },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: 'dropoff',
|
|
62
|
+
label: 'Dropoff / Destination',
|
|
63
|
+
fields: [
|
|
64
|
+
{ key: 'dropoff_name', label: 'Dropoff Name', required: false },
|
|
65
|
+
{ key: 'dropoff_street1', label: 'Dropoff Street 1', required: true },
|
|
66
|
+
{ key: 'dropoff_street2', label: 'Dropoff Street 2', required: false },
|
|
67
|
+
{ key: 'dropoff_city', label: 'Dropoff City', required: false },
|
|
68
|
+
{ key: 'dropoff_state', label: 'Dropoff State', required: false },
|
|
69
|
+
{ key: 'dropoff_postal_code', label: 'Dropoff Postal Code', required: false },
|
|
70
|
+
{ key: 'dropoff_country', label: 'Dropoff Country', required: false, hint: 'ISO 3166-1 alpha-2' },
|
|
71
|
+
{ key: 'dropoff_phone', label: 'Dropoff Phone', required: false },
|
|
72
|
+
{ key: 'dropoff_lat', label: 'Dropoff Latitude', required: false },
|
|
73
|
+
{ key: 'dropoff_lng', label: 'Dropoff Longitude', required: false },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: 'payload',
|
|
78
|
+
label: 'Payload',
|
|
79
|
+
fields: [
|
|
80
|
+
{ key: 'weight_kg', label: 'Weight (kg)', required: false },
|
|
81
|
+
{ key: 'volume_m3', label: 'Volume (m3)', required: false },
|
|
82
|
+
{ key: 'parcels', label: 'Parcels', required: false },
|
|
83
|
+
{ key: 'cod_amount', label: 'COD Amount', required: false },
|
|
84
|
+
{ key: 'cod_currency', label: 'COD Currency', required: false, hint: 'ISO 4217, e.g. USD' },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: 'customer',
|
|
89
|
+
label: 'Customer',
|
|
90
|
+
fields: [
|
|
91
|
+
{ key: 'customer_name', label: 'Customer Name', required: false },
|
|
92
|
+
{ key: 'customer_email', label: 'Customer Email', required: false, hint: 'Used to look up or create a contact' },
|
|
93
|
+
{ key: 'customer_phone', label: 'Customer Phone', required: false },
|
|
94
|
+
{ key: 'customer_type', label: 'Customer Type', required: false, hint: 'contact (default) or vendor' },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: 'facilitator',
|
|
99
|
+
label: 'Facilitator',
|
|
100
|
+
fields: [
|
|
101
|
+
{ key: 'facilitator_name', label: 'Facilitator Name', required: false },
|
|
102
|
+
{ key: 'facilitator_email', label: 'Facilitator Email', required: false, hint: 'Used to look up or create a vendor' },
|
|
103
|
+
{ key: 'facilitator_phone', label: 'Facilitator Phone', required: false },
|
|
104
|
+
{ key: 'facilitator_type', label: 'Facilitator Type', required: false, hint: 'contact or vendor (default)' },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: 'vehicle',
|
|
109
|
+
label: 'Vehicle',
|
|
110
|
+
fields: [{ key: 'vehicle_plate', label: 'Vehicle Plate', required: false, hint: 'Looks up vehicle by plate number' }],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
key: 'driver',
|
|
114
|
+
label: 'Driver',
|
|
115
|
+
fields: [
|
|
116
|
+
{ key: 'driver_name', label: 'Driver Name', required: false },
|
|
117
|
+
{ key: 'driver_phone', label: 'Driver Phone', required: false, hint: 'Used to look up driver' },
|
|
118
|
+
{ key: 'driver_email', label: 'Driver Email', required: false },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: 'entity',
|
|
123
|
+
label: 'Entity (Item / Parcel / Passenger)',
|
|
124
|
+
fields: [
|
|
125
|
+
{ key: 'entity_name', label: 'Entity Name', required: false, hint: 'Name of the item, parcel or passenger' },
|
|
126
|
+
{ key: 'entity_type', label: 'Entity Type', required: false, hint: 'e.g. parcel, passenger, pallet' },
|
|
127
|
+
{ key: 'entity_description', label: 'Entity Description', required: false },
|
|
128
|
+
{ key: 'entity_sku', label: 'Entity SKU', required: false },
|
|
129
|
+
{ key: 'entity_barcode', label: 'Entity Barcode', required: false },
|
|
130
|
+
{ key: 'entity_internal_id', label: 'Entity Internal ID', required: false },
|
|
131
|
+
{ key: 'entity_declared_value', label: 'Declared Value', required: false },
|
|
132
|
+
{ key: 'entity_currency', label: 'Entity Currency', required: false, hint: 'ISO 4217, e.g. USD' },
|
|
133
|
+
{ key: 'entity_price', label: 'Entity Price', required: false },
|
|
134
|
+
{ key: 'entity_sale_price', label: 'Entity Sale Price', required: false },
|
|
135
|
+
{ key: 'entity_weight', label: 'Entity Weight', required: false },
|
|
136
|
+
{ key: 'entity_weight_unit', label: 'Weight Unit', required: false, hint: 'kg, lb, g, oz' },
|
|
137
|
+
{ key: 'entity_length', label: 'Entity Length', required: false },
|
|
138
|
+
{ key: 'entity_width', label: 'Entity Width', required: false },
|
|
139
|
+
{ key: 'entity_height', label: 'Entity Height', required: false },
|
|
140
|
+
{ key: 'entity_dimensions_unit', label: 'Dimensions Unit', required: false, hint: 'cm, m, in, ft' },
|
|
141
|
+
{
|
|
142
|
+
key: 'entity_destination',
|
|
143
|
+
label: 'Entity Destination',
|
|
144
|
+
required: false,
|
|
145
|
+
hint: 'pickup, dropoff, or waypoint index (0,1,2…). Add extra rows with the same order_ref to attach multiple entities to one order.',
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
/** Flat list of all target fields (used for column-mapping state). */
|
|
152
|
+
const TARGET_FIELDS = FIELD_SECTIONS.flatMap((s) => s.fields);
|
|
153
|
+
|
|
154
|
+
/** Auto-mapping aliases: key => array of substrings to match against column names. */
|
|
155
|
+
const ALIASES = {
|
|
156
|
+
order_type: ['order type', 'order_type', 'type of order'],
|
|
157
|
+
order_ref: ['order ref', 'order_ref', 'ref', 'group', 'batch'],
|
|
158
|
+
internal_id: ['internal id', 'internal_id', 'internal ref'],
|
|
159
|
+
type: ['order config', 'config slug', 'order slug'],
|
|
160
|
+
status: ['status'],
|
|
161
|
+
scheduled_at: ['scheduled', 'delivery date', 'date', 'delivery time'],
|
|
162
|
+
notes: ['notes', 'instructions', 'remarks', 'reference'],
|
|
163
|
+
priority: ['priority', 'urgency'],
|
|
164
|
+
time_window_start: ['time window start', 'tw start', 'earliest', 'from time'],
|
|
165
|
+
time_window_end: ['time window end', 'tw end', 'latest', 'to time'],
|
|
166
|
+
service_time_min: ['service time', 'dwell time', 'stop time'],
|
|
167
|
+
required_skills: ['skills', 'requirements', 'required skills'],
|
|
168
|
+
pickup_name: ['pickup name', 'origin name', 'from name'],
|
|
169
|
+
pickup_street1: ['pickup street', 'pickup address', 'from address', 'origin address', 'collection address', 'pick up'],
|
|
170
|
+
pickup_street2: ['pickup street 2', 'pickup address 2'],
|
|
171
|
+
pickup_city: ['pickup city', 'origin city', 'from city'],
|
|
172
|
+
pickup_state: ['pickup state', 'origin state', 'from state'],
|
|
173
|
+
pickup_postal_code: ['pickup postal', 'pickup zip', 'origin postal'],
|
|
174
|
+
pickup_country: ['pickup country', 'origin country'],
|
|
175
|
+
pickup_phone: ['pickup phone', 'origin phone'],
|
|
176
|
+
pickup_lat: ['pickup lat', 'origin lat', 'from lat'],
|
|
177
|
+
pickup_lng: ['pickup lng', 'pickup lon', 'origin lng', 'from lng'],
|
|
178
|
+
|
|
179
|
+
dropoff_name: ['dropoff name', 'destination name', 'to name', 'delivery name'],
|
|
180
|
+
dropoff_street1: ['dropoff street', 'dropoff address', 'to address', 'destination address', 'delivery address', 'drop off'],
|
|
181
|
+
dropoff_street2: ['dropoff street 2', 'dropoff address 2'],
|
|
182
|
+
dropoff_city: ['dropoff city', 'destination city', 'to city'],
|
|
183
|
+
dropoff_state: ['dropoff state', 'destination state'],
|
|
184
|
+
dropoff_postal_code: ['dropoff postal', 'dropoff zip', 'destination postal'],
|
|
185
|
+
dropoff_country: ['dropoff country', 'destination country'],
|
|
186
|
+
dropoff_phone: ['dropoff phone', 'recipient phone'],
|
|
187
|
+
dropoff_lat: ['dropoff lat', 'destination lat', 'to lat'],
|
|
188
|
+
dropoff_lng: ['dropoff lng', 'dropoff lon', 'destination lng', 'to lng'],
|
|
189
|
+
|
|
190
|
+
weight_kg: ['weight', 'kg', 'weight kg', 'gross weight'],
|
|
191
|
+
volume_m3: ['volume', 'm3', 'cbm', 'cubic'],
|
|
192
|
+
parcels: ['parcels', 'packages', 'pieces'],
|
|
193
|
+
cod_amount: ['cod amount', 'cash on delivery', 'cod'],
|
|
194
|
+
cod_currency: ['cod currency', 'currency'],
|
|
195
|
+
customer_name: ['customer name', 'customer', 'recipient', 'consignee'],
|
|
196
|
+
customer_email: ['customer email', 'recipient email'],
|
|
197
|
+
customer_phone: ['customer phone', 'recipient phone', 'mobile'],
|
|
198
|
+
customer_type: ['customer type'],
|
|
199
|
+
facilitator_name: ['facilitator name', 'facilitator', 'vendor name', 'partner'],
|
|
200
|
+
facilitator_email: ['facilitator email', 'vendor email'],
|
|
201
|
+
facilitator_phone: ['facilitator phone', 'vendor phone'],
|
|
202
|
+
facilitator_type: ['facilitator type', 'vendor type'],
|
|
203
|
+
vehicle_plate: ['vehicle plate', 'plate number', 'plate', 'registration'],
|
|
204
|
+
driver_name: ['driver name', 'driver'],
|
|
205
|
+
driver_phone: ['driver phone', 'driver mobile'],
|
|
206
|
+
driver_email: ['driver email'],
|
|
207
|
+
entity_name: ['entity name', 'item name', 'parcel name', 'package name', 'passenger name', 'item', 'parcel'],
|
|
208
|
+
entity_type: ['entity type', 'item type', 'parcel type', 'package type'],
|
|
209
|
+
entity_description: ['entity description', 'item description', 'parcel description', 'description'],
|
|
210
|
+
entity_sku: ['sku', 'entity sku', 'item sku', 'product code'],
|
|
211
|
+
entity_barcode: ['barcode', 'entity barcode', 'item barcode'],
|
|
212
|
+
entity_internal_id: ['entity internal id', 'item id', 'parcel id'],
|
|
213
|
+
entity_declared_value: ['declared value', 'entity value', 'item value'],
|
|
214
|
+
entity_currency: ['entity currency', 'item currency'],
|
|
215
|
+
entity_price: ['entity price', 'item price', 'price'],
|
|
216
|
+
entity_sale_price: ['sale price', 'entity sale price'],
|
|
217
|
+
entity_weight: ['entity weight', 'item weight', 'parcel weight'],
|
|
218
|
+
entity_weight_unit: ['entity weight unit', 'weight unit'],
|
|
219
|
+
entity_length: ['entity length', 'item length', 'length'],
|
|
220
|
+
entity_width: ['entity width', 'item width', 'width'],
|
|
221
|
+
entity_height: ['entity height', 'item height', 'height'],
|
|
222
|
+
entity_dimensions_unit: ['dimensions unit', 'dim unit', 'entity dim unit'],
|
|
223
|
+
entity_destination: ['entity destination', 'item destination', 'deliver to', 'waypoint'],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/** Sample row for the downloadable template. */
|
|
227
|
+
const SAMPLE_ROW = {
|
|
228
|
+
order_type: 'pickup_dropoff',
|
|
229
|
+
order_ref: 'ORD-001',
|
|
230
|
+
internal_id: 'INT-001',
|
|
231
|
+
type: 'default',
|
|
232
|
+
status: 'created',
|
|
233
|
+
scheduled_at: '2026-05-01 09:00',
|
|
234
|
+
notes: 'Leave at front door',
|
|
235
|
+
priority: '50',
|
|
236
|
+
time_window_start: '08:00',
|
|
237
|
+
time_window_end: '12:00',
|
|
238
|
+
service_time_min: '10',
|
|
239
|
+
required_skills: '',
|
|
240
|
+
pickup_name: 'Warehouse A',
|
|
241
|
+
pickup_street1: '123 Warehouse Rd',
|
|
242
|
+
pickup_street2: '',
|
|
243
|
+
pickup_city: 'London',
|
|
244
|
+
pickup_state: 'England',
|
|
245
|
+
pickup_postal_code: 'E1 6RF',
|
|
246
|
+
pickup_country: 'GB',
|
|
247
|
+
pickup_phone: '+44 20 7946 0000',
|
|
248
|
+
pickup_lat: '',
|
|
249
|
+
pickup_lng: '',
|
|
250
|
+
dropoff_name: 'John Smith',
|
|
251
|
+
dropoff_street1: '456 High Street',
|
|
252
|
+
dropoff_street2: 'Apt 3B',
|
|
253
|
+
dropoff_city: 'London',
|
|
254
|
+
dropoff_state: 'England',
|
|
255
|
+
dropoff_postal_code: 'SW1A 1AA',
|
|
256
|
+
dropoff_country: 'GB',
|
|
257
|
+
dropoff_phone: '+44 7700 900000',
|
|
258
|
+
dropoff_lat: '',
|
|
259
|
+
dropoff_lng: '',
|
|
260
|
+
weight_kg: '5.2',
|
|
261
|
+
volume_m3: '0.04',
|
|
262
|
+
parcels: '1',
|
|
263
|
+
cod_amount: '',
|
|
264
|
+
cod_currency: '',
|
|
265
|
+
customer_name: 'John Smith',
|
|
266
|
+
customer_email: 'john.smith@example.com',
|
|
267
|
+
customer_phone: '+44 7700 900000',
|
|
268
|
+
customer_type: 'contact',
|
|
269
|
+
facilitator_name: '',
|
|
270
|
+
facilitator_email: '',
|
|
271
|
+
facilitator_phone: '',
|
|
272
|
+
facilitator_type: 'vendor',
|
|
273
|
+
vehicle_plate: '',
|
|
274
|
+
driver_name: '',
|
|
275
|
+
driver_phone: '',
|
|
276
|
+
driver_email: '',
|
|
277
|
+
entity_name: 'Widget A',
|
|
278
|
+
entity_type: 'parcel',
|
|
279
|
+
entity_description: '1x Widget A in box',
|
|
280
|
+
entity_sku: 'WGT-001',
|
|
281
|
+
entity_barcode: '',
|
|
282
|
+
entity_internal_id: '',
|
|
283
|
+
entity_declared_value: '25.00',
|
|
284
|
+
entity_currency: 'USD',
|
|
285
|
+
entity_price: '25.00',
|
|
286
|
+
entity_sale_price: '',
|
|
287
|
+
entity_weight: '0.5',
|
|
288
|
+
entity_weight_unit: 'kg',
|
|
289
|
+
entity_length: '',
|
|
290
|
+
entity_width: '',
|
|
291
|
+
entity_height: '',
|
|
292
|
+
entity_dimensions_unit: 'cm',
|
|
293
|
+
entity_destination: 'dropoff',
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
export default class OrchestratorImportComponent extends Component {
|
|
297
|
+
@service fetch;
|
|
298
|
+
@service notifications;
|
|
299
|
+
@service modalsManager;
|
|
300
|
+
@service intl;
|
|
301
|
+
|
|
302
|
+
// ── Step state ────────────────────────────────────────────────────────────
|
|
303
|
+
@tracked step = 'upload'; // 'upload' | 'map' | 'preview'
|
|
304
|
+
|
|
305
|
+
// ── Upload step ───────────────────────────────────────────────────────────
|
|
306
|
+
@tracked selectedFile = null;
|
|
307
|
+
@tracked isDraggingFile = false;
|
|
308
|
+
@tracked parseError = null;
|
|
309
|
+
@tracked fileColumns = [];
|
|
310
|
+
@tracked rawRows = [];
|
|
311
|
+
|
|
312
|
+
// ── Mapping step ──────────────────────────────────────────────────────────
|
|
313
|
+
/** Array of { key, label, required, mappedColumn, hint? } objects. */
|
|
314
|
+
@tracked columnMappings = TARGET_FIELDS.map((f) => ({ ...f, mappedColumn: null }));
|
|
315
|
+
|
|
316
|
+
// ── Preview step ──────────────────────────────────────────────────────────
|
|
317
|
+
@tracked mappedRows = [];
|
|
318
|
+
@tracked validationErrors = [];
|
|
319
|
+
|
|
320
|
+
// ── Sections (for grouped column-mapping UI) ──────────────────────────────
|
|
321
|
+
get fieldSections() {
|
|
322
|
+
return FIELD_SECTIONS.map((section) => ({
|
|
323
|
+
...section,
|
|
324
|
+
fields: this.columnMappings.filter((m) => section.fields.some((f) => f.key === m.key)),
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Preview groups (grouped by order_ref for the preview table) ──────────
|
|
329
|
+
/**
|
|
330
|
+
* Collapses mappedRows into one entry per logical order (grouped by order_ref).
|
|
331
|
+
* Each group carries summary fields used by the preview table.
|
|
332
|
+
*/
|
|
333
|
+
get previewGroups() {
|
|
334
|
+
const rows = this.mappedRows ?? [];
|
|
335
|
+
const groups = new Map();
|
|
336
|
+
|
|
337
|
+
rows.forEach((row) => {
|
|
338
|
+
const key = row.order_ref || row._rowIndex || String(Math.random());
|
|
339
|
+
if (!groups.has(key)) {
|
|
340
|
+
groups.set(key, {
|
|
341
|
+
// identity
|
|
342
|
+
order_ref: row.order_ref || null,
|
|
343
|
+
internal_id: row.internal_id || null,
|
|
344
|
+
order_type: row.order_type || 'pickup_dropoff',
|
|
345
|
+
// addresses
|
|
346
|
+
pickup_street1: row.pickup_street1 || null,
|
|
347
|
+
pickup_city: row.pickup_city || null,
|
|
348
|
+
dropoff_street1: row.dropoff_street1 || null,
|
|
349
|
+
dropoff_city: row.dropoff_city || null,
|
|
350
|
+
// parties
|
|
351
|
+
customer_name: row.customer_name || null,
|
|
352
|
+
customer_email: row.customer_email || null,
|
|
353
|
+
facilitator_name: row.facilitator_name || null,
|
|
354
|
+
facilitator_email: row.facilitator_email || null,
|
|
355
|
+
vehicle_plate: row.vehicle_plate || null,
|
|
356
|
+
driver_name: row.driver_name || null,
|
|
357
|
+
driver_email: row.driver_email || null,
|
|
358
|
+
// schedule
|
|
359
|
+
scheduled_at: row.scheduled_at || null,
|
|
360
|
+
// counters
|
|
361
|
+
waypointCount: 0,
|
|
362
|
+
entityCount: 0,
|
|
363
|
+
// validation
|
|
364
|
+
hasError: false,
|
|
365
|
+
errorMessage: null,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const g = groups.get(key);
|
|
369
|
+
|
|
370
|
+
// Count waypoint stops (rows with a dropoff address for multi_waypoint)
|
|
371
|
+
if (row.order_type === 'multi_waypoint' && row.dropoff_street1) {
|
|
372
|
+
g.waypointCount += 1;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Count entities (rows with entity_name)
|
|
376
|
+
if (row.entity_name) {
|
|
377
|
+
g.entityCount += 1;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Propagate errors
|
|
381
|
+
if (row._error) {
|
|
382
|
+
g.hasError = true;
|
|
383
|
+
g.errorMessage = row._error;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Fill in missing address/party fields from later rows in the group
|
|
387
|
+
if (!g.pickup_street1 && row.pickup_street1) g.pickup_street1 = row.pickup_street1;
|
|
388
|
+
if (!g.pickup_city && row.pickup_city) g.pickup_city = row.pickup_city;
|
|
389
|
+
if (!g.dropoff_street1 && row.dropoff_street1) g.dropoff_street1 = row.dropoff_street1;
|
|
390
|
+
if (!g.dropoff_city && row.dropoff_city) g.dropoff_city = row.dropoff_city;
|
|
391
|
+
if (!g.customer_name && row.customer_name) g.customer_name = row.customer_name;
|
|
392
|
+
if (!g.customer_email && row.customer_email) g.customer_email = row.customer_email;
|
|
393
|
+
if (!g.facilitator_name && row.facilitator_name) g.facilitator_name = row.facilitator_name;
|
|
394
|
+
if (!g.facilitator_email && row.facilitator_email) g.facilitator_email = row.facilitator_email;
|
|
395
|
+
if (!g.vehicle_plate && row.vehicle_plate) g.vehicle_plate = row.vehicle_plate;
|
|
396
|
+
if (!g.driver_name && row.driver_name) g.driver_name = row.driver_name;
|
|
397
|
+
if (!g.driver_email && row.driver_email) g.driver_email = row.driver_email;
|
|
398
|
+
if (!g.scheduled_at && row.scheduled_at) g.scheduled_at = row.scheduled_at;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return Array.from(groups.values());
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ── Footer action buttons (injected into modal footer via modalsManager) ──
|
|
405
|
+
/**
|
|
406
|
+
* Returns the correct footer button array for the current step.
|
|
407
|
+
* Called whenever the step changes via _setStep().
|
|
408
|
+
*/
|
|
409
|
+
get _footerButtons() {
|
|
410
|
+
const t = (key) => this.intl.t('orchestrator.' + key);
|
|
411
|
+
|
|
412
|
+
if (this.step === 'upload') {
|
|
413
|
+
return [
|
|
414
|
+
{
|
|
415
|
+
type: 'primary',
|
|
416
|
+
icon: 'arrow-right',
|
|
417
|
+
iconPosition: 'right',
|
|
418
|
+
wrapperClass: 'btn-icon-right',
|
|
419
|
+
text: t('next'),
|
|
420
|
+
disabled: !this.selectedFile,
|
|
421
|
+
isLoading: this.parseFile.isRunning,
|
|
422
|
+
perform: this.parseFile,
|
|
423
|
+
},
|
|
424
|
+
];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (this.step === 'map') {
|
|
428
|
+
return [
|
|
429
|
+
{
|
|
430
|
+
type: 'default',
|
|
431
|
+
icon: 'arrow-left',
|
|
432
|
+
iconPosition: 'left',
|
|
433
|
+
text: t('back'),
|
|
434
|
+
onClick: () => this._setStep('upload'),
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
type: 'primary',
|
|
438
|
+
icon: 'arrow-right',
|
|
439
|
+
iconPosition: 'right',
|
|
440
|
+
wrapperClass: 'btn-icon-right',
|
|
441
|
+
text: t('next'),
|
|
442
|
+
disabled: !this.mappingIsValid,
|
|
443
|
+
isLoading: this.buildPreview.isRunning,
|
|
444
|
+
perform: this.buildPreview,
|
|
445
|
+
},
|
|
446
|
+
];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (this.step === 'preview') {
|
|
450
|
+
const isRunning = this.submitImport.isRunning;
|
|
451
|
+
return [
|
|
452
|
+
{
|
|
453
|
+
type: 'default',
|
|
454
|
+
icon: 'arrow-left',
|
|
455
|
+
iconPosition: 'left',
|
|
456
|
+
text: t('back'),
|
|
457
|
+
disabled: isRunning,
|
|
458
|
+
onClick: () => this._setStep('map'),
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
type: 'success',
|
|
462
|
+
icon: isRunning ? 'spinner' : 'file-import',
|
|
463
|
+
text: isRunning ? t('importing') : t('import-confirm'),
|
|
464
|
+
disabled: isRunning,
|
|
465
|
+
isLoading: isRunning,
|
|
466
|
+
perform: this.submitImport,
|
|
467
|
+
},
|
|
468
|
+
];
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Update the modal footer buttons whenever the step changes. */
|
|
475
|
+
_syncFooterButtons() {
|
|
476
|
+
this.modalsManager.setOption('actionButtons', this._footerButtons);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Change step and sync footer buttons. */
|
|
480
|
+
_setStep(step) {
|
|
481
|
+
this.step = step;
|
|
482
|
+
this._syncFooterButtons();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ── Upload handlers ───────────────────────────────────────────────────────
|
|
486
|
+
/**
|
|
487
|
+
* Called by ember-file-upload's FileDropzone / UploadButton when a file is
|
|
488
|
+
* added to the queue. The `file` object is an ember-file-upload UploadFile
|
|
489
|
+
* wrapper — we extract the underlying native File from `file.file`.
|
|
490
|
+
*/
|
|
491
|
+
@action onFileQueued(file) {
|
|
492
|
+
const nativeFile = file?.file ?? file;
|
|
493
|
+
if (nativeFile instanceof File) {
|
|
494
|
+
this._setFile(nativeFile);
|
|
495
|
+
}
|
|
496
|
+
// Remove from the upload queue — we handle the file ourselves (no server upload).
|
|
497
|
+
file?.queue?.remove(file);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
@action onFileDragOver(event) {
|
|
501
|
+
event.preventDefault();
|
|
502
|
+
this.isDraggingFile = true;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
@action onFileDragLeave() {
|
|
506
|
+
this.isDraggingFile = false;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
@action onFileDrop(event) {
|
|
510
|
+
event.preventDefault();
|
|
511
|
+
this.isDraggingFile = false;
|
|
512
|
+
const file = event.dataTransfer?.files?.[0];
|
|
513
|
+
if (file) this._setFile(file);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
@action onFileSelected(event) {
|
|
517
|
+
const file = event.target?.files?.[0];
|
|
518
|
+
if (file) this._setFile(file);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
@action clearFile() {
|
|
522
|
+
this.selectedFile = null;
|
|
523
|
+
this.parseError = null;
|
|
524
|
+
this.fileColumns = [];
|
|
525
|
+
this.rawRows = [];
|
|
526
|
+
this._syncFooterButtons();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
_setFile(file) {
|
|
530
|
+
const allowed = ['text/csv', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'];
|
|
531
|
+
const ext = file.name.split('.').pop()?.toLowerCase();
|
|
532
|
+
if (!allowed.includes(file.type) && !['csv', 'xlsx', 'xls'].includes(ext)) {
|
|
533
|
+
this.parseError = this.intl.t('orchestrator.invalid-file-type');
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
this.selectedFile = file;
|
|
537
|
+
this.parseError = null;
|
|
538
|
+
this._syncFooterButtons();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
get formattedFileSize() {
|
|
542
|
+
if (!this.selectedFile) return '';
|
|
543
|
+
const bytes = this.selectedFile.size;
|
|
544
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
545
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
546
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ── Parse file ────────────────────────────────────────────────────────────
|
|
550
|
+
@task *parseFile() {
|
|
551
|
+
this.parseError = null;
|
|
552
|
+
try {
|
|
553
|
+
const ext = this.selectedFile.name.split('.').pop()?.toLowerCase();
|
|
554
|
+
let columns, rows;
|
|
555
|
+
if (ext === 'xlsx' || ext === 'xls') {
|
|
556
|
+
({ columns, rows } = yield this._parseXlsx(this.selectedFile));
|
|
557
|
+
} else {
|
|
558
|
+
const text = yield this._readFileAsText(this.selectedFile);
|
|
559
|
+
({ columns, rows } = this._parseCsv(text));
|
|
560
|
+
}
|
|
561
|
+
this.fileColumns = columns;
|
|
562
|
+
this.rawRows = rows;
|
|
563
|
+
this._autoMapColumns(columns);
|
|
564
|
+
this._setStep('map');
|
|
565
|
+
} catch (error) {
|
|
566
|
+
this.parseError = error.message ?? this.intl.t('orchestrator.parse-error');
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
_readFileAsText(file) {
|
|
571
|
+
return new Promise((resolve, reject) => {
|
|
572
|
+
const reader = new FileReader();
|
|
573
|
+
reader.onload = (e) => resolve(e.target.result);
|
|
574
|
+
reader.onerror = () => reject(new Error(this.intl.t('orchestrator.read-error')));
|
|
575
|
+
reader.readAsText(file);
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
_readFileAsArrayBuffer(file) {
|
|
580
|
+
return new Promise((resolve, reject) => {
|
|
581
|
+
const reader = new FileReader();
|
|
582
|
+
reader.onload = (e) => resolve(e.target.result);
|
|
583
|
+
reader.onerror = () => reject(new Error(this.intl.t('orchestrator.read-error')));
|
|
584
|
+
reader.readAsArrayBuffer(file);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async _parseXlsx(file) {
|
|
589
|
+
const buffer = await this._readFileAsArrayBuffer(file);
|
|
590
|
+
const workbook = XLSX.read(buffer, { type: 'array' });
|
|
591
|
+
// Use the first sheet
|
|
592
|
+
const sheetName = workbook.SheetNames[0];
|
|
593
|
+
const sheet = workbook.Sheets[sheetName];
|
|
594
|
+
// Convert to array of arrays (header + data rows)
|
|
595
|
+
const aoa = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' });
|
|
596
|
+
if (!aoa.length) throw new Error(this.intl.t('orchestrator.empty-file'));
|
|
597
|
+
// First row is the header — strip leading/trailing spaces and any trailing asterisks
|
|
598
|
+
const rawHeaders = aoa[0];
|
|
599
|
+
const columns = rawHeaders.map((h) =>
|
|
600
|
+
String(h)
|
|
601
|
+
.trim()
|
|
602
|
+
.replace(/\s*\*$/, '')
|
|
603
|
+
);
|
|
604
|
+
// Take up to 3 sample data rows
|
|
605
|
+
const rows = aoa.slice(1, 4).map((rowArr) => {
|
|
606
|
+
return Object.fromEntries(columns.map((col, i) => [col, rowArr[i] ?? '']));
|
|
607
|
+
});
|
|
608
|
+
return { columns, rows };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
_parseCsv(text) {
|
|
612
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
613
|
+
if (!lines.length) throw new Error(this.intl.t('orchestrator.empty-file'));
|
|
614
|
+
const columns = this._parseCsvLine(lines[0]);
|
|
615
|
+
const rows = lines.slice(1, 4).map((line) => {
|
|
616
|
+
const vals = this._parseCsvLine(line);
|
|
617
|
+
return Object.fromEntries(columns.map((col, i) => [col, vals[i] ?? '']));
|
|
618
|
+
});
|
|
619
|
+
return { columns, rows };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Parse a single CSV line, respecting quoted fields. */
|
|
623
|
+
_parseCsvLine(line) {
|
|
624
|
+
const result = [];
|
|
625
|
+
let current = '';
|
|
626
|
+
let inQuotes = false;
|
|
627
|
+
for (let i = 0; i < line.length; i++) {
|
|
628
|
+
const ch = line[i];
|
|
629
|
+
if (ch === '"') {
|
|
630
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
631
|
+
current += '"';
|
|
632
|
+
i++;
|
|
633
|
+
} else {
|
|
634
|
+
inQuotes = !inQuotes;
|
|
635
|
+
}
|
|
636
|
+
} else if (ch === ',' && !inQuotes) {
|
|
637
|
+
result.push(current.trim());
|
|
638
|
+
current = '';
|
|
639
|
+
} else {
|
|
640
|
+
current += ch;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
result.push(current.trim());
|
|
644
|
+
return result;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
_autoMapColumns(columns) {
|
|
648
|
+
this.columnMappings = this.columnMappings.map((mapping) => {
|
|
649
|
+
const aliases = ALIASES[mapping.key] ?? [];
|
|
650
|
+
const matched = columns.find((col) => aliases.some((alias) => col.toLowerCase().includes(alias)));
|
|
651
|
+
return { ...mapping, mappedColumn: matched ?? null };
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ── Column mapping helpers ────────────────────────────────────────────────
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Plain string options for the column-mapping selects.
|
|
659
|
+
* The empty string '' is used as the "skip" sentinel — the <Select>
|
|
660
|
+
* component renders it as the placeholder / skip option.
|
|
661
|
+
*/
|
|
662
|
+
get fileColumnOptions() {
|
|
663
|
+
// Return plain strings; '' means "skip / not mapped"
|
|
664
|
+
return ['', ...this.fileColumns];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
get previewRows() {
|
|
668
|
+
return this.rawRows;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Handle a column-mapping select change.
|
|
673
|
+
*
|
|
674
|
+
* Called via {{on "change" (fn this.setColumnMapping fieldKey)}} on a
|
|
675
|
+
* native <select> element, so the second argument is a DOM Event.
|
|
676
|
+
* We extract event.target.value to get the selected column string.
|
|
677
|
+
* An empty string means "skip / not mapped".
|
|
678
|
+
*/
|
|
679
|
+
@action setColumnMapping(fieldKey, event) {
|
|
680
|
+
const value = typeof event === 'string' ? event : (event?.target?.value ?? '');
|
|
681
|
+
const mapped = value === '' ? null : value;
|
|
682
|
+
this.columnMappings = this.columnMappings.map((m) => (m.key === fieldKey ? { ...m, mappedColumn: mapped } : m));
|
|
683
|
+
// Re-sync footer so "Next" disabled state is updated
|
|
684
|
+
this._syncFooterButtons();
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Number of fields that have been mapped to a spreadsheet column. */
|
|
688
|
+
get mappedCount() {
|
|
689
|
+
return this.columnMappings.filter((m) => m.mappedColumn).length;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** Total number of target fields. */
|
|
693
|
+
get totalFieldCount() {
|
|
694
|
+
return this.columnMappings.length;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
get mappingIsValid() {
|
|
698
|
+
// At minimum, dropoff_street1 must be mapped
|
|
699
|
+
return this.columnMappings.filter((m) => m.required).every((m) => m.mappedColumn);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ── Build preview ─────────────────────────────────────────────────────────
|
|
703
|
+
@task *buildPreview() {
|
|
704
|
+
try {
|
|
705
|
+
const ext = this.selectedFile.name.split('.').pop()?.toLowerCase();
|
|
706
|
+
let allRows;
|
|
707
|
+
|
|
708
|
+
if (ext === 'xlsx' || ext === 'xls') {
|
|
709
|
+
// Re-parse the full xlsx file (all rows, not just the 3-row sample)
|
|
710
|
+
const buffer = yield this._readFileAsArrayBuffer(this.selectedFile);
|
|
711
|
+
const workbook = XLSX.read(buffer, { type: 'array' });
|
|
712
|
+
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
|
713
|
+
const aoa = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' });
|
|
714
|
+
const rawHeaders = aoa[0];
|
|
715
|
+
const columns = rawHeaders.map((h) =>
|
|
716
|
+
String(h)
|
|
717
|
+
.trim()
|
|
718
|
+
.replace(/\s*\*$/, '')
|
|
719
|
+
);
|
|
720
|
+
allRows = aoa.slice(1).map((rowArr) => Object.fromEntries(columns.map((col, i) => [col, rowArr[i] ?? ''])));
|
|
721
|
+
} else {
|
|
722
|
+
const text = yield this._readFileAsText(this.selectedFile);
|
|
723
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
724
|
+
const columns = this._parseCsvLine(lines[0]);
|
|
725
|
+
allRows = lines.slice(1).map((line) => {
|
|
726
|
+
const vals = this._parseCsvLine(line);
|
|
727
|
+
return Object.fromEntries(columns.map((col, i) => [col, vals[i] ?? '']));
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const mapped = allRows.map((row, idx) => {
|
|
732
|
+
const result = { _rowIndex: idx + 2 };
|
|
733
|
+
for (const m of this.columnMappings) {
|
|
734
|
+
if (m.mappedColumn) {
|
|
735
|
+
result[m.key] = row[m.mappedColumn] ?? '';
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// Validate: need at least a dropoff street address
|
|
739
|
+
if (!result.dropoff_street1) {
|
|
740
|
+
result._error = this.intl.t('orchestrator.missing-dropoff');
|
|
741
|
+
}
|
|
742
|
+
return result;
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
this.mappedRows = mapped;
|
|
746
|
+
this.validationErrors = mapped.filter((r) => r._error);
|
|
747
|
+
this._setStep('preview');
|
|
748
|
+
} catch (error) {
|
|
749
|
+
this.notifications.serverError(error);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ── Submit import ────────────────────────────────────────────────
|
|
754
|
+
@task *submitImport() {
|
|
755
|
+
const validRows = this.mappedRows.filter((r) => !r._error);
|
|
756
|
+
if (!validRows.length) {
|
|
757
|
+
this.notifications.warning(this.intl.t('orchestrator.no-valid-rows'));
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
// Immediately push loading state to the footer button so the user
|
|
761
|
+
// gets instant visual feedback and cannot double-click.
|
|
762
|
+
this._syncFooterButtons();
|
|
763
|
+
try {
|
|
764
|
+
yield this.fetch.post('fleet-ops/orchestrator/import-orders', {
|
|
765
|
+
rows: validRows,
|
|
766
|
+
options: { mark_imported: true },
|
|
767
|
+
});
|
|
768
|
+
this.notifications.success(this.intl.t('orchestrator.import-success', { count: validRows.length }));
|
|
769
|
+
if (typeof this.args.options?.onImportComplete === 'function') {
|
|
770
|
+
this.args.options.onImportComplete();
|
|
771
|
+
}
|
|
772
|
+
if (typeof this.args.onConfirm === 'function') {
|
|
773
|
+
this.args.onConfirm();
|
|
774
|
+
}
|
|
775
|
+
} catch (error) {
|
|
776
|
+
this.notifications.serverError(error);
|
|
777
|
+
// Restore the button to its normal state on error so the user
|
|
778
|
+
// can retry without having to navigate away.
|
|
779
|
+
this._syncFooterButtons();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ── Navigation ────────────────────────────────────────────────────────────
|
|
784
|
+
@action goToUpload() {
|
|
785
|
+
this._setStep('upload');
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
@action goToMapping() {
|
|
789
|
+
this._setStep('map');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ── Template download ─────────────────────────────────────────────────────
|
|
793
|
+
@action downloadTemplate() {
|
|
794
|
+
if (typeof this.args.options?.onImportTemplate === 'function') {
|
|
795
|
+
return this.args.options.onImportTemplate();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Build a dynamic XLSX template from the current field list + a sample row
|
|
799
|
+
// so the template always stays in sync with the importer field definitions.
|
|
800
|
+
const headers = TARGET_FIELDS.map((f) => f.key);
|
|
801
|
+
const sampleValues = headers.map((k) => SAMPLE_ROW[k] ?? '');
|
|
802
|
+
const ws = XLSX.utils.aoa_to_sheet([headers, sampleValues]);
|
|
803
|
+
const wb = XLSX.utils.book_new();
|
|
804
|
+
XLSX.utils.book_append_sheet(wb, ws, 'Orders');
|
|
805
|
+
XLSX.writeFile(wb, 'Fleetbase_Order_Import_Template.xlsx');
|
|
806
|
+
}
|
|
807
|
+
}
|