@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
|
@@ -2,384 +2,908 @@ import Controller from '@ember/controller';
|
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
3
|
import { inject as service } from '@ember/service';
|
|
4
4
|
import { action, computed } from '@ember/object';
|
|
5
|
-
import {
|
|
5
|
+
import { isNone } from '@ember/utils';
|
|
6
|
+
import { isValid as isValidDate } from 'date-fns';
|
|
6
7
|
import { task } from 'ember-concurrency';
|
|
7
|
-
import { format, isValid as isValidDate } from 'date-fns';
|
|
8
|
-
import { Tooltip } from '@fleetbase/ember-ui/utils/floating';
|
|
9
8
|
import isObject from '@fleetbase/ember-core/utils/is-object';
|
|
10
9
|
import isJson from '@fleetbase/ember-core/utils/is-json';
|
|
11
|
-
import createFullCalendarEventFromOrder
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
10
|
+
import createFullCalendarEventFromOrder from '../../../utils/create-full-calendar-event-from-order';
|
|
11
|
+
import createFullCalendarEventFromScheduleItem from '../../../utils/create-full-calendar-event-from-schedule-item';
|
|
12
|
+
import toCalendarDate from '../../../utils/to-calendar-date';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* OperationsSchedulerIndexController
|
|
16
|
+
*
|
|
17
|
+
* Unified order dispatch board controller.
|
|
18
|
+
* All scheduling domain logic is delegated to the injected `scheduling` service.
|
|
19
|
+
*
|
|
20
|
+
* Calendar library: @event-calendar/core (MIT licensed).
|
|
21
|
+
* This replaces FullCalendar Premium resource-timeline plugins which are
|
|
22
|
+
* incompatible with Fleetbase's dual AGPL v3 / commercial license.
|
|
23
|
+
*
|
|
24
|
+
* Timezone handling
|
|
25
|
+
* -----------------
|
|
26
|
+
* @event-calendar/core has no timezone support — it reads the browser-local
|
|
27
|
+
* fields of any Date and positions events at that wall-clock time (see
|
|
28
|
+
* https://github.com/vkurko/calendar/issues/576).
|
|
29
|
+
*
|
|
30
|
+
* The solution (as recommended by the maintainer) is to convert all UTC dates
|
|
31
|
+
* to "fake local" Dates whose local fields equal the company wall-clock time
|
|
32
|
+
* before passing them to the calendar. This is done via `toCalendarDate()`.
|
|
33
|
+
*
|
|
34
|
+
* The same conversion is applied to:
|
|
35
|
+
* - Event start/end (order events and shift background blocks)
|
|
36
|
+
* - The `now` option (current-time indicator position)
|
|
37
|
+
* - The `date` option (which day is highlighted as "today")
|
|
38
|
+
*
|
|
39
|
+
* When the user drops or drags an event, the calendar returns a Date whose
|
|
40
|
+
* local fields equal the visible wall-clock time. `_reinterpretDateInTimezone`
|
|
41
|
+
* converts that back to a true UTC instant for the API.
|
|
42
|
+
*
|
|
43
|
+
* Data flow:
|
|
44
|
+
* Route -> store.query() -> Ember Data store
|
|
45
|
+
* Controller computed getters -> store.peekAll() -> reactive UI
|
|
46
|
+
* Socket service -> store.pushPayload() -> reactive UI (no page refresh)
|
|
47
|
+
*
|
|
48
|
+
* External drag-and-drop:
|
|
49
|
+
* Sidebar cards use native HTML5 draggable="true".
|
|
50
|
+
* onSidebarDragStart stores the dragged order reference.
|
|
51
|
+
* onCalendarDrop uses calendar.dateFromPoint(x, y) to resolve the target
|
|
52
|
+
* date and resource, then delegates to SchedulingService.assignOrder().
|
|
53
|
+
*/
|
|
40
54
|
export default class OperationsSchedulerIndexController extends Controller {
|
|
41
|
-
@service
|
|
42
|
-
@service
|
|
55
|
+
@service scheduling;
|
|
56
|
+
@service socket;
|
|
43
57
|
@service store;
|
|
58
|
+
@service notifications;
|
|
59
|
+
@service modalsManager;
|
|
60
|
+
@service currentUser;
|
|
44
61
|
@service intl;
|
|
45
|
-
@service
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@tracked
|
|
62
|
+
@service fetch;
|
|
63
|
+
|
|
64
|
+
// UI State
|
|
65
|
+
@tracked calendar = null;
|
|
66
|
+
@tracked viewDate = new Date();
|
|
67
|
+
@tracked viewRange = 'week';
|
|
68
|
+
@tracked searchQuery = '';
|
|
69
|
+
@tracked activeFilters = [];
|
|
70
|
+
@tracked selectedOrderIds = new Set();
|
|
49
71
|
@tracked drivers = [];
|
|
50
|
-
@tracked
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
@tracked sidebarCollapsed = false;
|
|
73
|
+
|
|
74
|
+
// Holds the order being dragged from the sidebar so onCalendarDrop can access it.
|
|
75
|
+
_draggedOrder = null;
|
|
76
|
+
|
|
77
|
+
// Revision counter — incremented after every successful save so that
|
|
78
|
+
// @computed('_orderRevision') getters recompute even when Ember Data's
|
|
79
|
+
// @each tracking misses a deep attribute change on an existing record.
|
|
80
|
+
@tracked _orderRevision = 0;
|
|
81
|
+
|
|
82
|
+
// -------------------------------------------------------------------------
|
|
83
|
+
// Reactive Computed Getters
|
|
84
|
+
// -------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
@computed('_orderRevision', 'store')
|
|
87
|
+
get allActiveOrders() {
|
|
88
|
+
// Return all orders with an active status — no date window filtering.
|
|
89
|
+
// The calendar renders only what falls in the visible range; unscheduled
|
|
90
|
+
// orders appear in the sidebar panel. Past orders are kept for historical
|
|
91
|
+
// context and future orders are always visible regardless of current view.
|
|
92
|
+
const statuses = ['created', 'dispatched', 'active'];
|
|
93
|
+
return this.store.peekAll('order').filter((order) => statuses.includes(order.status));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@computed('allActiveOrders.@each.scheduled_at', 'searchQuery', 'searchQuery.length', 'activeFilters.[]')
|
|
97
|
+
get unscheduledOrders() {
|
|
98
|
+
let orders = this.allActiveOrders.filter((o) => isNone(o.scheduled_at) || !isValidDate(new Date(o.scheduled_at)));
|
|
99
|
+
if (this.searchQuery && this.searchQuery.length >= 2) {
|
|
100
|
+
const q = this.searchQuery.toLowerCase();
|
|
101
|
+
orders = orders.filter((o) => {
|
|
102
|
+
return (o.public_id ?? '').toLowerCase().includes(q) || (o.tracking ?? '').toLowerCase().includes(q) || (o.payload?.dropoff?.address ?? '').toLowerCase().includes(q);
|
|
58
103
|
});
|
|
59
104
|
}
|
|
60
|
-
|
|
105
|
+
this.activeFilters.forEach((filter) => {
|
|
106
|
+
if (filter.type === 'type') orders = orders.filter((o) => o.type === filter.value);
|
|
107
|
+
if (filter.type === 'priority') orders = orders.filter((o) => o.priority === filter.value);
|
|
108
|
+
});
|
|
109
|
+
return orders;
|
|
61
110
|
}
|
|
62
111
|
|
|
63
|
-
@computed('
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
extendedProps: { driver },
|
|
68
|
-
}));
|
|
112
|
+
@computed('allActiveOrders.@each.{scheduled_at,driver_assigned_uuid,status}', 'currentUser.company.timezone', 'companyTimezone')
|
|
113
|
+
get calendarEvents() {
|
|
114
|
+
const tz = this.companyTimezone;
|
|
115
|
+
return this.allActiveOrders.filter((o) => !isNone(o.scheduled_at) && isValidDate(new Date(o.scheduled_at))).map((o) => createFullCalendarEventFromOrder(o, tz));
|
|
69
116
|
}
|
|
70
117
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
118
|
+
@computed('drivers.[]', 'allActiveOrders.@each.{scheduled_at,driver_assigned_uuid}')
|
|
119
|
+
get calendarResources() {
|
|
120
|
+
return this.drivers.map((driver) => {
|
|
121
|
+
const assignedCount = this.allActiveOrders.filter((o) => o.driver_assigned_uuid === driver.id && !isNone(o.scheduled_at)).length;
|
|
122
|
+
const maxCapacity = driver.max_daily_orders ?? 10;
|
|
123
|
+
const pct = Math.round((assignedCount / maxCapacity) * 100);
|
|
124
|
+
return {
|
|
125
|
+
id: driver.id,
|
|
126
|
+
title: driver.name,
|
|
127
|
+
extendedProps: {
|
|
128
|
+
driver,
|
|
129
|
+
workload: { assigned: assignedCount, capacity: maxCapacity, percentage: Math.min(pct, 100) },
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
});
|
|
76
133
|
}
|
|
77
134
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
135
|
+
@computed('drivers.@each.currentShift', 'currentUser.company.timezone', 'companyTimezone')
|
|
136
|
+
get backgroundEvents() {
|
|
137
|
+
const tz = this.companyTimezone;
|
|
138
|
+
const events = [];
|
|
139
|
+
this.drivers.forEach((driver) => {
|
|
140
|
+
const shift = driver.currentShift;
|
|
141
|
+
if (shift) {
|
|
142
|
+
events.push(
|
|
143
|
+
createFullCalendarEventFromScheduleItem(shift, driver, tz, {
|
|
144
|
+
display: 'background',
|
|
145
|
+
backgroundColor: 'rgba(99, 102, 241, 0.08)',
|
|
146
|
+
borderColor: 'rgba(99, 102, 241, 0.25)',
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return events;
|
|
81
152
|
}
|
|
82
153
|
|
|
83
|
-
@
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
this.drivers = drivers.toArray();
|
|
87
|
-
} catch (error) {
|
|
88
|
-
this.notifications.serverError(error);
|
|
89
|
-
}
|
|
154
|
+
@computed('calendarEvents.[]', 'backgroundEvents.[]')
|
|
155
|
+
get allCalendarEvents() {
|
|
156
|
+
return [...this.calendarEvents, ...this.backgroundEvents];
|
|
90
157
|
}
|
|
91
158
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
159
|
+
/**
|
|
160
|
+
* The view name string passed to EventCalendar's @view arg.
|
|
161
|
+
* @event-calendar/core uses 'resourceTimelineDay' / 'resourceTimelineWeek'
|
|
162
|
+
* identical to FullCalendar's naming convention.
|
|
163
|
+
*/
|
|
164
|
+
get currentCalendarView() {
|
|
165
|
+
const viewMap = { day: 'resourceTimelineDay', week: 'resourceTimelineWeek' };
|
|
166
|
+
return viewMap[this.viewRange] ?? 'resourceTimelineDay';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Minimal header toolbar — only the date title is shown.
|
|
171
|
+
* Navigation (prev/next/today) and view-range buttons are already
|
|
172
|
+
* provided by the section header above the calendar, so we suppress
|
|
173
|
+
* the duplicates here.
|
|
174
|
+
*/
|
|
175
|
+
get calendarHeaderToolbar() {
|
|
176
|
+
return { start: '', center: 'title', end: '' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Returns the IANA timezone string for the current organisation.
|
|
181
|
+
* Falls back to the browser's local timezone when the company record has
|
|
182
|
+
* not yet loaded or has no timezone set.
|
|
183
|
+
*
|
|
184
|
+
* @returns {string} e.g. 'Asia/Singapore', 'America/New_York'
|
|
185
|
+
*/
|
|
186
|
+
get companyTimezone() {
|
|
187
|
+
return this.currentUser?.company?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Options passed to the EventCalendar @options arg.
|
|
192
|
+
* These control display formatting only — timezone conversion is handled
|
|
193
|
+
* by toCalendarDate() before events reach the calendar.
|
|
194
|
+
*
|
|
195
|
+
* @returns {object}
|
|
196
|
+
*/
|
|
197
|
+
@computed('currentUser.company.timezone')
|
|
198
|
+
get calendarOptions() {
|
|
199
|
+
return {
|
|
200
|
+
slotLabelFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
|
|
201
|
+
eventTimeFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
|
|
202
|
+
dayHeaderFormat: { weekday: 'short', month: 'numeric', day: 'numeric' },
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The current moment expressed as a "fake local" Date in the company
|
|
208
|
+
* timezone. Passed to the calendar as the `now` option so that the
|
|
209
|
+
* current-time indicator appears at the correct position on the timeline.
|
|
210
|
+
*
|
|
211
|
+
* @returns {Date}
|
|
212
|
+
*/
|
|
213
|
+
@computed('currentUser.company.timezone', 'companyTimezone')
|
|
214
|
+
get calendarNow() {
|
|
215
|
+
return toCalendarDate(new Date(), this.companyTimezone);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Full 24-hour day visible on the timeline. */
|
|
219
|
+
get calendarSlotMinTime() {
|
|
220
|
+
return '00:00:00';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
get calendarSlotMaxTime() {
|
|
224
|
+
return '24:00:00';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// -------------------------------------------------------------------------
|
|
228
|
+
// EventCalendar Render Hooks
|
|
229
|
+
// -------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Renders the resource label cell for each driver row.
|
|
233
|
+
* Returns an HTML string that EventCalendar injects into the label cell.
|
|
234
|
+
* Shows driver name and a full-width capacity bar.
|
|
235
|
+
*/
|
|
236
|
+
@action renderResourceLabel({ resource }) {
|
|
237
|
+
const { driver, workload } = resource.extendedProps ?? {};
|
|
238
|
+
if (!driver) return resource.title ?? '';
|
|
239
|
+
const { assigned = 0, capacity = 10, percentage = 0 } = workload ?? {};
|
|
240
|
+
const barColour = percentage >= 90 ? '#ef4444' : percentage >= 70 ? '#f59e0b' : '#6366f1';
|
|
241
|
+
return {
|
|
242
|
+
html: `<div class="ec-resource-label-inner" style="width:100%;box-sizing:border-box;padding:4px 8px;">
|
|
243
|
+
<div style="font-size:0.75rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:100%;">${driver.name ?? ''}</div>
|
|
244
|
+
<div style="display:flex;align-items:center;gap:4px;margin-top:3px;width:100%;">
|
|
245
|
+
<div style="flex:1;min-width:0;height:4px;background:#374151;border-radius:9999px;overflow:hidden;">
|
|
246
|
+
<div style="height:100%;width:${percentage}%;background:${barColour};border-radius:9999px;transition:width 0.3s;"></div>
|
|
247
|
+
</div>
|
|
248
|
+
<span style="font-size:0.625rem;color:#9ca3af;white-space:nowrap;flex-shrink:0;">${assigned}/${capacity}</span>
|
|
249
|
+
</div>
|
|
250
|
+
</div>`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Renders the event tile content inside the timeline.
|
|
256
|
+
* Returns an HTML string for order events; shift background events render
|
|
257
|
+
* with no custom content (EventCalendar handles background display natively).
|
|
258
|
+
* Shows: tracking number, status badge, driver name, scheduled time, destination.
|
|
259
|
+
*/
|
|
260
|
+
@action renderEventContent({ event }) {
|
|
261
|
+
if (event.display === 'background') return null;
|
|
262
|
+
const { order, status } = event.extendedProps ?? {};
|
|
263
|
+
const tracking = event.title ?? order?.tracking ?? order?.public_id ?? '';
|
|
264
|
+
const driverName = order?.driver_assigned?.name ?? order?.get?.('driver_assigned.name') ?? '';
|
|
265
|
+
const destination = order?.pickupName ?? order?.get?.('pickupName') ?? '';
|
|
266
|
+
const scheduledTime = order?.scheduledAtTime ?? order?.get?.('scheduledAtTime') ?? '';
|
|
267
|
+
const statusLabel = status ? status.charAt(0).toUpperCase() + status.slice(1) : '';
|
|
268
|
+
const metaLine = [scheduledTime, driverName].filter(Boolean).join(' · ');
|
|
269
|
+
return {
|
|
270
|
+
html: `<div style="display:flex;flex-direction:column;gap:2px;padding:2px 0;overflow:hidden;height:100%;">
|
|
271
|
+
<div style="display:flex;align-items:center;gap:4px;flex-wrap:nowrap;">
|
|
272
|
+
<span style="width:7px;height:7px;border-radius:50%;background:#ffffff;opacity:0.9;flex-shrink:0;"></span>
|
|
273
|
+
<span style="font-size:0.72rem;font-weight:700;color:#ffffff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;">${tracking}</span>
|
|
274
|
+
<span style="font-size:0.58rem;background:rgba(255,255,255,0.2);color:#ffffff;border-radius:3px;padding:1px 4px;white-space:nowrap;flex-shrink:0;">${statusLabel}</span>
|
|
275
|
+
</div>
|
|
276
|
+
${metaLine ? `<div style="font-size:0.65rem;color:rgba(255,255,255,0.85);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${metaLine}</div>` : ''}
|
|
277
|
+
${destination ? `<div style="font-size:0.65rem;color:rgba(255,255,255,0.7);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">→ ${destination}</div>` : ''}
|
|
278
|
+
</div>`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// -------------------------------------------------------------------------
|
|
283
|
+
// Sidebar Selection
|
|
284
|
+
// -------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
get selectedOrders() {
|
|
287
|
+
return this.unscheduledOrders.filter((o) => this.selectedOrderIds.has(o.id));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
get hasSelection() {
|
|
291
|
+
return this.selectedOrderIds.size > 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@action isOrderSelected(orderId) {
|
|
295
|
+
return this.selectedOrderIds.has(orderId);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@action toggleOrderSelection(orderId) {
|
|
299
|
+
const next = new Set(this.selectedOrderIds);
|
|
300
|
+
next.has(orderId) ? next.delete(orderId) : next.add(orderId);
|
|
301
|
+
this.selectedOrderIds = next;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
@action selectAllOrders() {
|
|
305
|
+
this.selectedOrderIds = new Set(this.unscheduledOrders.map((o) => o.id));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
@action clearSelection() {
|
|
309
|
+
this.selectedOrderIds = new Set();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// -------------------------------------------------------------------------
|
|
313
|
+
// Debounced Sidebar Search
|
|
314
|
+
// -------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
@task({ restartable: true })
|
|
317
|
+
*searchTask(query) {
|
|
318
|
+
yield new Promise((resolve) => setTimeout(resolve, 300));
|
|
319
|
+
this.searchQuery = query;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@action onSearchInput(event) {
|
|
323
|
+
this.searchTask.perform(event.target.value);
|
|
103
324
|
}
|
|
104
325
|
|
|
326
|
+
@action clearSearch() {
|
|
327
|
+
this.searchQuery = '';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// -------------------------------------------------------------------------
|
|
331
|
+
// EventCalendar Lifecycle
|
|
332
|
+
// -------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Receives the EventCalendar instance once it is mounted.
|
|
336
|
+
* The instance exposes: setOption(), getOption(), prev(), next(),
|
|
337
|
+
* getEventById(), removeEventById(), updateEvent(), dateFromPoint().
|
|
338
|
+
*/
|
|
105
339
|
@action setCalendarApi(calendar) {
|
|
106
340
|
this.calendar = calendar;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// -------------------------------------------------------------------------
|
|
344
|
+
// Drag-and-Drop: External Drop from Sidebar (native HTML5)
|
|
345
|
+
// -------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Called on dragstart for each sidebar order card.
|
|
349
|
+
* Stores the order reference so onCalendarDrop can retrieve it.
|
|
350
|
+
*/
|
|
351
|
+
@action onSidebarDragStart(order, event) {
|
|
352
|
+
this._draggedOrder = order;
|
|
353
|
+
// Set a minimal dataTransfer payload as a fallback identifier.
|
|
354
|
+
event.dataTransfer.setData('text/plain', order.id);
|
|
355
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
356
|
+
}
|
|
111
357
|
|
|
112
|
-
|
|
113
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Prevents the browser's default "no drop" behaviour so the drop event fires.
|
|
360
|
+
* Also sets a data attribute on the timeline container to trigger the CSS
|
|
361
|
+
* drag-over highlight, and moves a thin cursor line to the exact drop column.
|
|
362
|
+
*/
|
|
363
|
+
@action onCalendarDragOver(event) {
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
event.dataTransfer.dropEffect = 'move';
|
|
366
|
+
const el = document.getElementById('fleet-ops-scheduler-timeline');
|
|
367
|
+
if (!el) return;
|
|
368
|
+
el.dataset.draggingOver = 'true';
|
|
369
|
+
// Move the drop-cursor indicator to the current pointer X position.
|
|
370
|
+
const ecMain = el.querySelector('.ec-main');
|
|
371
|
+
if (ecMain) {
|
|
372
|
+
let cursor = ecMain.querySelector('.ec-drop-cursor');
|
|
373
|
+
if (!cursor) {
|
|
374
|
+
cursor = document.createElement('div');
|
|
375
|
+
cursor.className = 'ec-drop-cursor';
|
|
376
|
+
ecMain.style.position = 'relative';
|
|
377
|
+
ecMain.appendChild(cursor);
|
|
378
|
+
}
|
|
379
|
+
const rect = ecMain.getBoundingClientRect();
|
|
380
|
+
const scrollLeft = ecMain.scrollLeft;
|
|
381
|
+
cursor.style.left = event.clientX - rect.left + scrollLeft + 'px';
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Clears the drag-over highlight and removes the cursor indicator when the
|
|
387
|
+
* pointer genuinely exits the timeline container (not just moves to a child).
|
|
388
|
+
*/
|
|
389
|
+
@action onCalendarDragLeave(event) {
|
|
390
|
+
const el = document.getElementById('fleet-ops-scheduler-timeline');
|
|
391
|
+
if (el && !el.contains(event.relatedTarget)) {
|
|
392
|
+
delete el.dataset.draggingOver;
|
|
393
|
+
const cursor = el.querySelector('.ec-drop-cursor');
|
|
394
|
+
if (cursor) cursor.remove();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Handles a sidebar card being dropped onto the EventCalendar timeline.
|
|
400
|
+
* Uses calendar.dateFromPoint(x, y) to resolve the target date and resource
|
|
401
|
+
* from the drop coordinates — this is the @event-calendar/core equivalent
|
|
402
|
+
* of FullCalendar's onDrop / eventReceive callback.
|
|
403
|
+
*
|
|
404
|
+
* dateFromPoint() returns a Date whose LOCAL fields equal the wall-clock
|
|
405
|
+
* time the user sees on screen (because the calendar stored the "fake local"
|
|
406
|
+
* dates we passed in, and returns them the same way). We reinterpret those
|
|
407
|
+
* local fields as a true UTC instant using the company timezone.
|
|
408
|
+
*/
|
|
409
|
+
@action async onCalendarDrop(event) {
|
|
410
|
+
event.preventDefault();
|
|
411
|
+
// Clear the drag-over highlight and cursor indicator.
|
|
412
|
+
const timelineEl = document.getElementById('fleet-ops-scheduler-timeline');
|
|
413
|
+
if (timelineEl) {
|
|
414
|
+
delete timelineEl.dataset.draggingOver;
|
|
415
|
+
const cursor = timelineEl.querySelector('.ec-drop-cursor');
|
|
416
|
+
if (cursor) cursor.remove();
|
|
417
|
+
}
|
|
418
|
+
const order = this._draggedOrder;
|
|
419
|
+
this._draggedOrder = null;
|
|
420
|
+
if (!order || !this.calendar) return;
|
|
421
|
+
|
|
422
|
+
// Preserve scroll position so the drop doesn't reset the timeline view.
|
|
423
|
+
const ecMain = timelineEl?.querySelector('.ec-main');
|
|
424
|
+
const savedScrollLeft = ecMain ? ecMain.scrollLeft : 0;
|
|
425
|
+
const savedScrollTop = ecMain ? ecMain.scrollTop : 0;
|
|
426
|
+
|
|
427
|
+
// Resolve drop position to a date + resource using EventCalendar's API.
|
|
428
|
+
const dropInfo = this.calendar.dateFromPoint(event.clientX, event.clientY);
|
|
429
|
+
if (!dropInfo) return;
|
|
430
|
+
const { date, resource } = dropInfo;
|
|
431
|
+
const driverId = resource?.id ?? null;
|
|
432
|
+
|
|
433
|
+
// dateFromPoint() returns a Date whose local fields equal the wall-clock
|
|
434
|
+
// time visible on screen. Convert to a true UTC instant for the API.
|
|
435
|
+
const scheduledAt = date ? this._reinterpretDateInTimezone(date, this.companyTimezone) : new Date();
|
|
436
|
+
|
|
437
|
+
const result = await this.scheduling.assignOrder(order, driverId, scheduledAt);
|
|
438
|
+
|
|
439
|
+
// Bump the revision counter so allActiveOrders / unscheduledOrders
|
|
440
|
+
// recompute immediately — this removes the order from the sidebar.
|
|
441
|
+
if (!result.error) {
|
|
442
|
+
this._orderRevision += 1;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Restore scroll position after the calendar re-renders.
|
|
446
|
+
if (ecMain) {
|
|
447
|
+
requestAnimationFrame(() => {
|
|
448
|
+
ecMain.scrollLeft = savedScrollLeft;
|
|
449
|
+
ecMain.scrollTop = savedScrollTop;
|
|
114
450
|
});
|
|
115
|
-
}
|
|
451
|
+
}
|
|
116
452
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
453
|
+
if (result.hasConflict) {
|
|
454
|
+
this._showConflictModal(order, driverId, scheduledAt, result.conflicts);
|
|
455
|
+
}
|
|
120
456
|
}
|
|
121
457
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
458
|
+
// -------------------------------------------------------------------------
|
|
459
|
+
// Drag-and-Drop: Reschedule Existing Event (internal timeline drag)
|
|
460
|
+
// -------------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Handles an existing calendar event being dragged to a new time/resource.
|
|
464
|
+
* @event-calendar/core eventDrop info shape:
|
|
465
|
+
* { event, oldEvent, oldResource, newResource, delta, revert, jsEvent, view }
|
|
466
|
+
* event.resourceIds[0] replaces FullCalendar's event.getResources()[0]?.id
|
|
467
|
+
*/
|
|
468
|
+
@action async rescheduleEventFromDrag(info) {
|
|
469
|
+
const { event, revert } = info;
|
|
470
|
+
const { start, end, extendedProps } = event;
|
|
471
|
+
const tz = this.companyTimezone;
|
|
472
|
+
if (extendedProps?.scheduleItem) {
|
|
473
|
+
// Shift block drag — update the ScheduleItem record directly.
|
|
474
|
+
const scheduleItem = extendedProps.scheduleItem;
|
|
475
|
+
const newResourceId = event.resourceIds?.[0];
|
|
476
|
+
try {
|
|
477
|
+
scheduleItem.set('start_at', this._reinterpretDateInTimezone(start, tz));
|
|
478
|
+
scheduleItem.set('end_at', this._reinterpretDateInTimezone(end ?? start, tz));
|
|
479
|
+
if (newResourceId) scheduleItem.set('assignee_uuid', newResourceId);
|
|
480
|
+
await scheduleItem.save();
|
|
481
|
+
this.notifications.success(this.intl.t('scheduler.shift-updated'));
|
|
482
|
+
} catch (error) {
|
|
483
|
+
this.notifications.serverError(error);
|
|
484
|
+
revert();
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
// Order event drag — delegate to SchedulingService.
|
|
489
|
+
const order = this.store.peekRecord('order', event.id);
|
|
490
|
+
if (!order) return;
|
|
491
|
+
const newDriverId = event.resourceIds?.[0] ?? order.driver_assigned_uuid;
|
|
492
|
+
const result = await this.scheduling.assignOrder(order, newDriverId, this._reinterpretDateInTimezone(start, tz));
|
|
493
|
+
const tzStart = this._reinterpretDateInTimezone(start, tz);
|
|
494
|
+
if (result.hasConflict) {
|
|
495
|
+
revert();
|
|
496
|
+
this._showConflictModal(order, newDriverId, tzStart, result.conflicts);
|
|
497
|
+
} else if (result.error) {
|
|
498
|
+
revert();
|
|
499
|
+
} else {
|
|
500
|
+
this._orderRevision += 1;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// -------------------------------------------------------------------------
|
|
505
|
+
// Event Click
|
|
506
|
+
// -------------------------------------------------------------------------
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* @event-calendar/core eventClick info shape: { event, el, jsEvent, view }
|
|
510
|
+
* Identical to FullCalendar — no changes needed to the info object access.
|
|
511
|
+
*/
|
|
512
|
+
@action viewOrderAsEvent(info) {
|
|
513
|
+
const { event } = info;
|
|
514
|
+
if (event.extendedProps?.scheduleItem) return this._viewShiftEvent(event);
|
|
515
|
+
const order = this.store.peekRecord('order', event.id);
|
|
516
|
+
if (order) this.viewEvent(order);
|
|
517
|
+
}
|
|
125
518
|
|
|
519
|
+
@action viewEvent(order) {
|
|
126
520
|
this.modalsManager.show('modals/order-event', {
|
|
127
|
-
title: this.intl.t('scheduler.scheduling-for', { orderId: order.tracking }),
|
|
128
|
-
acceptButtonText: '
|
|
521
|
+
title: this.intl.t('scheduler.scheduling-for', { orderId: order.tracking ?? order.public_id }),
|
|
522
|
+
acceptButtonText: this.intl.t('common.save-changes'),
|
|
129
523
|
acceptButtonIcon: 'save',
|
|
130
524
|
hideDeclineButton: true,
|
|
131
525
|
order,
|
|
132
526
|
reschedule: (date) => {
|
|
133
|
-
if (date && typeof date.toDate === 'function')
|
|
134
|
-
date = date.toDate();
|
|
135
|
-
}
|
|
136
|
-
|
|
527
|
+
if (date && typeof date.toDate === 'function') date = date.toDate();
|
|
137
528
|
order.set('scheduled_at', date);
|
|
138
529
|
},
|
|
139
|
-
unschedule: () => {
|
|
140
|
-
|
|
530
|
+
unschedule: async (modalsManager, done) => {
|
|
531
|
+
modalsManager.startLoading();
|
|
532
|
+
await this.scheduling.unscheduleOrder(order);
|
|
533
|
+
done();
|
|
141
534
|
},
|
|
142
|
-
confirm: async (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (!order.get('hasDirtyAttributes')) {
|
|
146
|
-
return modal.done();
|
|
147
|
-
}
|
|
148
|
-
|
|
535
|
+
confirm: async (modalsManager, done) => {
|
|
536
|
+
modalsManager.startLoading();
|
|
537
|
+
if (!order.get('hasDirtyAttributes')) return done();
|
|
149
538
|
try {
|
|
150
539
|
await order.save();
|
|
151
|
-
// remove event from calendar
|
|
152
|
-
if (event) {
|
|
153
|
-
this.removeEvent(event);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
540
|
if (order.scheduled_at) {
|
|
157
|
-
|
|
158
|
-
this.notifications.success(this.intl.t('scheduler.info-message', { orderId: order.public_id, orderAt: order.scheduledAt }));
|
|
159
|
-
// add event to calendar
|
|
160
|
-
event = this.calendar.addEvent(createFullCalendarEventFromOrder(order));
|
|
541
|
+
this.notifications.success(this.intl.t('scheduler.success-message', { orderId: order.public_id, orderAt: order.scheduledAt }));
|
|
161
542
|
} else {
|
|
162
543
|
this.notifications.info(this.intl.t('scheduler.info-message', { orderId: order.public_id }));
|
|
163
544
|
}
|
|
164
|
-
|
|
165
|
-
// update event props
|
|
166
|
-
this.setEventProperty(event, 'title', createOrderEventTitle(order));
|
|
167
|
-
this.setEventProperty(event, 'description', createOrderEventDescription(order));
|
|
168
|
-
|
|
169
|
-
// refresh route
|
|
170
|
-
return this.hostRouter.refresh();
|
|
545
|
+
done();
|
|
171
546
|
} catch (error) {
|
|
172
547
|
this.notifications.serverError(error);
|
|
173
|
-
|
|
548
|
+
modalsManager.stopLoading();
|
|
174
549
|
}
|
|
175
550
|
},
|
|
176
551
|
});
|
|
177
552
|
}
|
|
178
553
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (mode === 'drivers') {
|
|
182
|
-
await this.loadDrivers.perform();
|
|
183
|
-
await this.loadScheduleItems.perform();
|
|
184
|
-
later(() => {
|
|
185
|
-
if (this.calendar) {
|
|
186
|
-
this.calendar.changeView('resourceTimelineWeek');
|
|
187
|
-
}
|
|
188
|
-
}, 100);
|
|
189
|
-
} else {
|
|
190
|
-
later(() => {
|
|
191
|
-
if (this.calendar) {
|
|
192
|
-
this.calendar.changeView('dayGridMonth');
|
|
193
|
-
}
|
|
194
|
-
}, 100);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
@action viewOrderAsEvent(eventClickInfo) {
|
|
199
|
-
const { event } = eventClickInfo;
|
|
200
|
-
if (event.extendedProps && event.extendedProps.scheduleItem) {
|
|
201
|
-
return this.viewScheduleItem(event.extendedProps.scheduleItem, event.extendedProps.driver);
|
|
202
|
-
}
|
|
203
|
-
const order = this.store.peekRecord('order', event.id);
|
|
204
|
-
this.viewEvent(order, eventClickInfo);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
@action viewScheduleItem(scheduleItem, driver) {
|
|
554
|
+
_viewShiftEvent(event) {
|
|
555
|
+
const { scheduleItem, driver } = event.extendedProps;
|
|
208
556
|
this.modalsManager.show('modals/driver-shift', {
|
|
209
|
-
title: `${driver.name}
|
|
210
|
-
acceptButtonText: 'Save Changes',
|
|
211
|
-
acceptButtonIcon: 'save',
|
|
557
|
+
title: driver ? `${driver.name} — ${this.intl.t('scheduler.shift')}` : this.intl.t('scheduler.shift'),
|
|
212
558
|
scheduleItem,
|
|
213
559
|
driver,
|
|
214
|
-
confirm: async (
|
|
215
|
-
|
|
560
|
+
confirm: async (modalsManager, done) => {
|
|
561
|
+
modalsManager.startLoading();
|
|
216
562
|
try {
|
|
217
563
|
await scheduleItem.save();
|
|
218
|
-
this.notifications.success('
|
|
219
|
-
|
|
220
|
-
modal.done();
|
|
564
|
+
this.notifications.success(this.intl.t('scheduler.shift-updated'));
|
|
565
|
+
done();
|
|
221
566
|
} catch (error) {
|
|
222
567
|
this.notifications.serverError(error);
|
|
223
|
-
|
|
568
|
+
modalsManager.stopLoading();
|
|
224
569
|
}
|
|
225
570
|
},
|
|
226
|
-
delete: async (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
571
|
+
delete: async (modalsManager, done) => {
|
|
572
|
+
modalsManager.startLoading();
|
|
573
|
+
try {
|
|
574
|
+
await scheduleItem.destroyRecord();
|
|
575
|
+
this.notifications.success(this.intl.t('scheduler.shift-deleted'));
|
|
576
|
+
done();
|
|
577
|
+
} catch (error) {
|
|
578
|
+
this.notifications.serverError(error);
|
|
579
|
+
modalsManager.stopLoading();
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// -------------------------------------------------------------------------
|
|
586
|
+
// Add Driver Shift
|
|
587
|
+
// -------------------------------------------------------------------------
|
|
588
|
+
|
|
589
|
+
@action addDriverShift() {
|
|
590
|
+
this.modalsManager.show('modals/add-driver-shift', {
|
|
591
|
+
title: this.intl.t('scheduler.add-shift'),
|
|
592
|
+
acceptButtonText: this.intl.t('scheduler.create-shift'),
|
|
593
|
+
acceptButtonIcon: 'plus',
|
|
594
|
+
drivers: this.drivers,
|
|
595
|
+
confirm: async (modalsManager, done) => {
|
|
596
|
+
modalsManager.startLoading();
|
|
597
|
+
const options = modalsManager.getOptions();
|
|
598
|
+
const targetDriver = options.selectedDriver;
|
|
599
|
+
try {
|
|
600
|
+
if (options.isRecurring) {
|
|
601
|
+
const template = this.store.createRecord('schedule-template', {
|
|
602
|
+
name: options.templateName || `${targetDriver?.name} Recurring Schedule`,
|
|
603
|
+
rrule: options.rrule,
|
|
604
|
+
start_time: options.shiftStartTime,
|
|
605
|
+
end_time: options.shiftEndTime,
|
|
606
|
+
break_start_time: options.breakStartTime || null,
|
|
607
|
+
break_end_time: options.breakEndTime || null,
|
|
608
|
+
color: options.templateColor || '#6366f1',
|
|
609
|
+
});
|
|
610
|
+
const savedTemplate = await template.save();
|
|
611
|
+
const schedules = await this.store.query('schedule', { subject_type: 'driver', subject_uuid: targetDriver.id, limit: 1 });
|
|
612
|
+
let schedule;
|
|
613
|
+
if (schedules.length > 0) {
|
|
614
|
+
schedule = schedules.firstObject;
|
|
615
|
+
} else {
|
|
616
|
+
schedule = await this.store
|
|
617
|
+
.createRecord('schedule', {
|
|
618
|
+
subject_type: 'driver',
|
|
619
|
+
subject_uuid: targetDriver.id,
|
|
620
|
+
name: `${targetDriver.name} Schedule`,
|
|
621
|
+
timezone: this.companyTimezone,
|
|
622
|
+
status: 'draft',
|
|
623
|
+
})
|
|
624
|
+
.save();
|
|
625
|
+
}
|
|
626
|
+
await this.fetch.post(`schedule-templates/${savedTemplate.id}/apply`, {
|
|
627
|
+
subject_type: 'driver',
|
|
628
|
+
subject_uuid: targetDriver.id,
|
|
629
|
+
schedule_uuid: schedule.id,
|
|
630
|
+
effective_from: options.recurrenceStartDate || new Date().toISOString(),
|
|
631
|
+
effective_until: options.recurrenceEndDate || null,
|
|
632
|
+
});
|
|
633
|
+
this.notifications.success(this.intl.t('scheduler.recurring-schedule-created'));
|
|
634
|
+
} else {
|
|
635
|
+
const scheduleItem = this.store.createRecord('schedule-item', {
|
|
636
|
+
assignee_type: 'driver',
|
|
637
|
+
assignee_uuid: targetDriver?.id,
|
|
638
|
+
title: options.title || null,
|
|
639
|
+
start_at: options.startAt,
|
|
640
|
+
end_at: options.endAt,
|
|
641
|
+
notes: options.notes || null,
|
|
642
|
+
status: 'scheduled',
|
|
643
|
+
});
|
|
644
|
+
await scheduleItem.save();
|
|
645
|
+
this.notifications.success(this.intl.t('scheduler.shift-created'));
|
|
237
646
|
}
|
|
647
|
+
done();
|
|
648
|
+
} catch (error) {
|
|
649
|
+
this.notifications.serverError(error);
|
|
650
|
+
modalsManager.stopLoading();
|
|
238
651
|
}
|
|
239
652
|
},
|
|
240
653
|
});
|
|
241
654
|
}
|
|
242
655
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const { event } = dataset;
|
|
247
|
-
const data = JSON.parse(event);
|
|
248
|
-
const order = this.store.peekRecord('order', data.id);
|
|
656
|
+
// -------------------------------------------------------------------------
|
|
657
|
+
// Bulk Operations
|
|
658
|
+
// -------------------------------------------------------------------------
|
|
249
659
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
this.
|
|
257
|
-
|
|
660
|
+
@action openBulkAssignModal() {
|
|
661
|
+
if (!this.hasSelection) return;
|
|
662
|
+
const orders = this.selectedOrders;
|
|
663
|
+
this.modalsManager.show('modals/bulk-assign-orders', {
|
|
664
|
+
title: this.intl.t('scheduler.bulk-assign-title', { count: orders.length }),
|
|
665
|
+
orders,
|
|
666
|
+
drivers: this.drivers,
|
|
667
|
+
confirm: async (modalsManager, done) => {
|
|
668
|
+
modalsManager.startLoading();
|
|
669
|
+
const { driver, date } = modalsManager.getOptions();
|
|
670
|
+
try {
|
|
671
|
+
await this.scheduling.bulkAssign(orders, driver.id, date);
|
|
672
|
+
this.clearSelection();
|
|
673
|
+
this.notifications.success(this.intl.t('scheduler.bulk-assign-success', { count: orders.length }));
|
|
674
|
+
done();
|
|
675
|
+
} catch (error) {
|
|
676
|
+
this.notifications.serverError(error);
|
|
677
|
+
modalsManager.stopLoading();
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
});
|
|
258
681
|
}
|
|
259
682
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
683
|
+
// -------------------------------------------------------------------------
|
|
684
|
+
// Conflict Resolution
|
|
685
|
+
// -------------------------------------------------------------------------
|
|
263
686
|
|
|
264
|
-
|
|
265
|
-
this.
|
|
687
|
+
_showConflictModal(order, driverId, scheduledAt, conflicts) {
|
|
688
|
+
const driver = this.store.peekRecord('driver', driverId);
|
|
689
|
+
this.modalsManager.show('modals/scheduling-conflict', {
|
|
690
|
+
title: this.intl.t('scheduler.conflict-title'),
|
|
691
|
+
order,
|
|
692
|
+
driver,
|
|
693
|
+
conflicts,
|
|
694
|
+
scheduledAt,
|
|
695
|
+
assignAnyway: async (modalsManager, done) => {
|
|
696
|
+
modalsManager.startLoading();
|
|
697
|
+
await this.scheduling.assignOrder(order, driverId, scheduledAt, { skipConflictCheck: true });
|
|
698
|
+
done();
|
|
699
|
+
},
|
|
700
|
+
autoAdjust: async (modalsManager, done) => {
|
|
701
|
+
modalsManager.startLoading();
|
|
702
|
+
const bestFit = await this.scheduling.findBestFit(driverId, order);
|
|
703
|
+
await this.scheduling.assignOrder(order, driverId, bestFit, { skipConflictCheck: true });
|
|
704
|
+
done();
|
|
705
|
+
},
|
|
706
|
+
});
|
|
266
707
|
}
|
|
267
708
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
709
|
+
// -------------------------------------------------------------------------
|
|
710
|
+
// Undo / Redo
|
|
711
|
+
// -------------------------------------------------------------------------
|
|
271
712
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
713
|
+
@action undo() {
|
|
714
|
+
return this.scheduling.undo();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
@action redo() {
|
|
718
|
+
return this.scheduling.redo();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// -------------------------------------------------------------------------
|
|
722
|
+
// Real-Time Socket Subscriptions
|
|
723
|
+
// -------------------------------------------------------------------------
|
|
724
|
+
|
|
725
|
+
@action async subscribeToRealTimeUpdates() {
|
|
726
|
+
const orgId = this.currentUser?.companyId ?? this.currentUser?.company?.id;
|
|
727
|
+
if (!orgId) return;
|
|
728
|
+
await this.socket.listen(`company.${orgId}.orders`, (payload) => this._handleOrderSocketEvent(payload));
|
|
729
|
+
this.drivers.forEach(async (driver) => {
|
|
730
|
+
await this.socket.listen(`driver.${driver.id}`, (payload) => this._handleDriverSocketEvent(payload));
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
@action unsubscribeFromRealTimeUpdates() {
|
|
735
|
+
if (this.socket && typeof this.socket.closeChannels === 'function') {
|
|
736
|
+
this.socket.closeChannels();
|
|
289
737
|
}
|
|
738
|
+
}
|
|
290
739
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
740
|
+
_handleOrderSocketEvent({ data } = {}) {
|
|
741
|
+
if (!data?.id) return;
|
|
742
|
+
try {
|
|
743
|
+
this.store.pushPayload('order', { order: data });
|
|
744
|
+
} catch {
|
|
745
|
+
/* ignore */
|
|
746
|
+
}
|
|
747
|
+
}
|
|
294
748
|
|
|
749
|
+
_handleDriverSocketEvent({ event, data } = {}) {
|
|
750
|
+
if (!data?.id) return;
|
|
751
|
+
if (event === 'driver.location_changed') return;
|
|
295
752
|
try {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
this.setEventProperty(event, 'description', createOrderEventDescription(order));
|
|
300
|
-
return this.hostRouter.refresh();
|
|
301
|
-
} catch (error) {
|
|
302
|
-
this.notifications.serverError(error);
|
|
303
|
-
this.removeEvent(event);
|
|
753
|
+
this.store.pushPayload('driver', { driver: data });
|
|
754
|
+
} catch {
|
|
755
|
+
/* ignore */
|
|
304
756
|
}
|
|
305
757
|
}
|
|
306
758
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
duration: duration,
|
|
323
|
-
status: 'pending',
|
|
324
|
-
});
|
|
325
|
-
await scheduleItem.save();
|
|
326
|
-
this.notifications.success('Shift created successfully');
|
|
327
|
-
await this.loadScheduleItems.perform();
|
|
328
|
-
modal.done();
|
|
329
|
-
} catch (error) {
|
|
330
|
-
this.notifications.serverError(error);
|
|
331
|
-
modal.stopLoading();
|
|
332
|
-
}
|
|
333
|
-
},
|
|
334
|
-
});
|
|
759
|
+
// -------------------------------------------------------------------------
|
|
760
|
+
// View Navigation
|
|
761
|
+
// -------------------------------------------------------------------------
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Navigation uses EventCalendar's setOption/getOption API:
|
|
765
|
+
* calendar.setOption('date', newDate) replaces calendar.today() / gotoDate()
|
|
766
|
+
* calendar.getOption('date') replaces calendar.getDate()
|
|
767
|
+
* calendar.setOption('view', viewName) replaces calendar.changeView()
|
|
768
|
+
* calendar.prev() / calendar.next() are identical in both libraries
|
|
769
|
+
*/
|
|
770
|
+
@action goToToday() {
|
|
771
|
+
// Use the company-local "today" so the calendar highlights the correct day.
|
|
772
|
+
this.viewDate = toCalendarDate(new Date(), this.companyTimezone);
|
|
773
|
+
this.calendar?.setOption('date', this.viewDate);
|
|
335
774
|
}
|
|
336
775
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
776
|
+
@action goToPrev() {
|
|
777
|
+
this.calendar?.prev();
|
|
778
|
+
const d = this.calendar?.getOption('date');
|
|
779
|
+
if (d) this.viewDate = d;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
@action goToNext() {
|
|
783
|
+
this.calendar?.next();
|
|
784
|
+
const d = this.calendar?.getOption('date');
|
|
785
|
+
if (d) this.viewDate = d;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
@action setViewRange(range) {
|
|
789
|
+
this.viewRange = range;
|
|
790
|
+
// currentCalendarView getter returns the correct view name string.
|
|
791
|
+
// EventCalendar re-renders reactively when @view arg changes, but we
|
|
792
|
+
// also call setOption for immediate imperative update if needed.
|
|
793
|
+
this.calendar?.setOption('view', this.currentCalendarView);
|
|
794
|
+
}
|
|
342
795
|
|
|
796
|
+
// -------------------------------------------------------------------------
|
|
797
|
+
// Legacy helpers (adapted for @event-calendar/core API)
|
|
798
|
+
// -------------------------------------------------------------------------
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Removes an event from the calendar by ID.
|
|
802
|
+
* @event-calendar/core uses removeEventById(id) instead of event.remove().
|
|
803
|
+
*/
|
|
804
|
+
removeEvent(event) {
|
|
343
805
|
if (isObject(event) && typeof event.id === 'string') {
|
|
344
|
-
|
|
806
|
+
this.calendar?.removeEventById(event.id);
|
|
807
|
+
return true;
|
|
345
808
|
}
|
|
346
|
-
|
|
347
809
|
if (isJson(event)) {
|
|
348
810
|
event = JSON.parse(event);
|
|
349
|
-
|
|
811
|
+
this.calendar?.removeEventById(event.id);
|
|
812
|
+
return true;
|
|
350
813
|
}
|
|
351
|
-
|
|
352
814
|
if (typeof event === 'string') {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
event.remove();
|
|
356
|
-
return true;
|
|
357
|
-
}
|
|
815
|
+
this.calendar?.removeEventById(event);
|
|
816
|
+
return true;
|
|
358
817
|
}
|
|
359
|
-
|
|
360
818
|
return false;
|
|
361
819
|
}
|
|
362
820
|
|
|
821
|
+
/**
|
|
822
|
+
* Retrieves an event object from the calendar by ID.
|
|
823
|
+
* @event-calendar/core uses getEventById(id) — same method name as FullCalendar.
|
|
824
|
+
*/
|
|
363
825
|
getEvent(event) {
|
|
364
826
|
if (isJson(event)) {
|
|
365
827
|
event = JSON.parse(event);
|
|
366
|
-
return this.calendar
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (typeof event === 'string') {
|
|
370
|
-
return this.calendar.getEventById(event);
|
|
828
|
+
return this.calendar?.getEventById(event.id);
|
|
371
829
|
}
|
|
372
|
-
|
|
830
|
+
if (typeof event === 'string') return this.calendar?.getEventById(event);
|
|
373
831
|
return event;
|
|
374
832
|
}
|
|
375
833
|
|
|
834
|
+
/**
|
|
835
|
+
* Updates a single property on a calendar event.
|
|
836
|
+
* @event-calendar/core uses updateEvent({...event, [prop]: value})
|
|
837
|
+
* instead of FullCalendar's event.setProp(prop, value).
|
|
838
|
+
*/
|
|
376
839
|
setEventProperty(event, prop, value) {
|
|
377
840
|
const eventInstance = this.getEvent(event);
|
|
378
|
-
if (
|
|
379
|
-
|
|
841
|
+
if (eventInstance) {
|
|
842
|
+
this.calendar?.updateEvent({ ...eventInstance, [prop]: value });
|
|
380
843
|
return true;
|
|
381
844
|
}
|
|
382
|
-
|
|
383
845
|
return false;
|
|
384
846
|
}
|
|
847
|
+
|
|
848
|
+
// -------------------------------------------------------------------------
|
|
849
|
+
// Timezone Utilities
|
|
850
|
+
// -------------------------------------------------------------------------
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Converts a Date whose **local** fields represent a wall-clock time back
|
|
854
|
+
* into the correct UTC instant for that moment in the given timezone.
|
|
855
|
+
*
|
|
856
|
+
* This is the inverse of toCalendarDate().
|
|
857
|
+
*
|
|
858
|
+
* When the calendar returns a Date from dateFromPoint() or an eventDrop
|
|
859
|
+
* callback, its local fields equal the wall-clock time the user sees on
|
|
860
|
+
* screen (because we passed "fake local" Dates in and the library echoes
|
|
861
|
+
* them back the same way). We must convert those local fields to a true
|
|
862
|
+
* UTC instant before sending to the API.
|
|
863
|
+
*
|
|
864
|
+
* Example: user drops at 22:30 on Apr 6 (visible on screen, SGT)
|
|
865
|
+
* date.getHours() === 22, date.getDate() === 6
|
|
866
|
+
* → returns a Date whose getUTCHours() === 14 (22:30 SGT = 14:30 UTC)
|
|
867
|
+
*
|
|
868
|
+
* @param {Date} date The Date returned by dateFromPoint() or eventDrop.
|
|
869
|
+
* @param {string} timezone IANA timezone string, e.g. 'Asia/Singapore'.
|
|
870
|
+
* @returns {Date}
|
|
871
|
+
*/
|
|
872
|
+
_reinterpretDateInTimezone(date, timezone) {
|
|
873
|
+
try {
|
|
874
|
+
// Read the local fields — these hold the wall-clock time the user
|
|
875
|
+
// sees on screen.
|
|
876
|
+
const y = date.getFullYear();
|
|
877
|
+
const mo = date.getMonth() + 1;
|
|
878
|
+
const d = date.getDate();
|
|
879
|
+
const h = date.getHours();
|
|
880
|
+
const mi = date.getMinutes();
|
|
881
|
+
const s = date.getSeconds();
|
|
882
|
+
|
|
883
|
+
// Build a UTC probe at the same wall-clock instant and ask Intl
|
|
884
|
+
// what offset the target timezone applies at that moment.
|
|
885
|
+
const wallClock = `${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}T${String(h).padStart(2, '0')}:${String(mi).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
|
886
|
+
const probe = new Date(`${wallClock}Z`);
|
|
887
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
888
|
+
timeZone: timezone,
|
|
889
|
+
year: 'numeric',
|
|
890
|
+
month: '2-digit',
|
|
891
|
+
day: '2-digit',
|
|
892
|
+
hour: '2-digit',
|
|
893
|
+
minute: '2-digit',
|
|
894
|
+
second: '2-digit',
|
|
895
|
+
hour12: false,
|
|
896
|
+
}).formatToParts(probe);
|
|
897
|
+
|
|
898
|
+
const get = (type) => parseInt(parts.find((p) => p.type === type)?.value ?? '0', 10);
|
|
899
|
+
const tzHour = get('hour') % 24;
|
|
900
|
+
const probeLocal = Date.UTC(get('year'), get('month') - 1, get('day'), tzHour, get('minute'), get('second'));
|
|
901
|
+
const offsetMs = probe.getTime() - probeLocal;
|
|
902
|
+
|
|
903
|
+
const wallMs = Date.UTC(y, mo - 1, d, h, mi, s);
|
|
904
|
+
return new Date(wallMs + offsetMs);
|
|
905
|
+
} catch {
|
|
906
|
+
return date;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
385
909
|
}
|