From a6508845a78d5227b82010013c7694019d9ccf1a Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 2 Jul 2026 11:24:08 -0400 Subject: [PATCH] =?UTF-8?q?feat(dispatch):=20VRP=20route=20optimizer=20(OR?= =?UTF-8?q?-Tools)=20=E2=80=94=20"Optimiser"=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real vehicle-routing optimization behind the Suggérer UI, reusing our existing OR-Tools solver service (no new stack). Fixes the greedy's structural limits (sector-splitting, no global optimization). - roster-solver: new route_solver.py (OR-Tools Routing / VRPTW). Minimizes real travel; skills = hard filter (VehicleVar ∈ allowed∪{-1}; SetAllowedVehiclesForIndex has a broken Span typemap in ortools 9.15); on-site service time within each tech's shift window; optional per-job time windows; unfittable jobs left unassigned (drop penalty) instead of infeasible; specialist bias via skill order (per-vehicle arc cost). New POST /route endpoint. Dockerfile now COPYs route_solver.py. Unit-tested: skills respected, sectors consolidated, edge cases safe. - hub: POST /roster/optimize-routes → proxies to solver /route. - ops SPA: "⚡ Optimiser" strategy. Greedy buckets jobs into days + placeholders, then the solver re-optimizes each day (assignment + routes) among shifted techs; result maps into the same review dialog (occupation bars, route map, swap/merge reused). Graceful fallback to greedy if the solver is unreachable (never drops jobs). shiftWindowMin derives each vehicle's shift from templates. Phase 2 (later): OSRM/Mapbox road-time matrix for exact travel times. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/api/roster.js | 2 + apps/ops/src/pages/PlanificationPage.vue | 47 +++++- services/roster-solver/Dockerfile | 2 +- services/roster-solver/app.py | 23 ++- services/roster-solver/route_solver.py | 200 +++++++++++++++++++++++ services/targo-hub/lib/roster.js | 8 + 6 files changed, 276 insertions(+), 6 deletions(-) create mode 100644 services/roster-solver/route_solver.py diff --git a/apps/ops/src/api/roster.js b/apps/ops/src/api/roster.js index abbd15b..d6e3384 100644 --- a/apps/ops/src/api/roster.js +++ b/apps/ops/src/api/roster.js @@ -98,6 +98,8 @@ export const jobCandidates = (job, exclude) => jget('/roster/job-candidates?job= export const redistributePlan = (plan) => jpost('/roster/skill-impact/redistribute', { plan }) // Jobs non assignés (+ groupe/dépendances) pour le panneau glisser-déposer export const unassignedJobs = () => jget('/roster/unassigned-jobs') +// Optimisation de tournées (VRPTW + compétences + temps sur place) via le solveur OR-Tools. payload = { jobs[], vehicles[], ... } +export const optimizeRoutes = (payload) => jpost('/roster/optimize-routes', payload) // Write-back legacy : aperçu (0 écriture) puis application (réassigne ticket.assign_to au tech dans osTicket) export const pushLegacyPreview = () => jget('/dispatch/legacy-sync/push-assignments') export const pushLegacyApply = (notify = true) => jpost('/dispatch/legacy-sync/push-assignments' + (notify ? '' : '?notify=0'), {}) diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index b76f103..c1bcbd9 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -975,8 +975,8 @@