From 3513f7e15f23bdd8ffa5e0fa77be37ce8221b6e1 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 2 Jul 2026 14:03:11 -0400 Subject: [PATCH] =?UTF-8?q?feat(dispatch):=20solver=20settings=20(distance?= =?UTF-8?q?=E2=86=94specialty,=20speed,=20time)=20+=20urgent=20priority?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Config UI (Optimiser mode): live settings — a distance↔specialty slider (rank_weight 0-10), fallback speed km/h, and solve-time budget. Persisted to localStorage, passed to the solver per run → tune the curve without redeploying. - Urgent jobs prioritized: each job's priority maps to priority_boost (drop penalty) — urgent/high never dropped, low priority dropped first when capacity is tight. SPA-only (solver already accepts rank_weight/speed_kmh/max_seconds/ priority_boost). Verified: settings panel renders, optimize runs end-to-end (16 techs, OSRM). Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/pages/PlanificationPage.vue | 30 +++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index f9bbf41..5c2715e 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -978,6 +978,23 @@ {{ ({ optimize: 'solveur VRP (OR-Tools) : routes réelles minimisées + compétences + temps sur place — regroupe les secteurs, spécialistes d\'abord', smart: 'proximité + compétence + charge', best: 'concentre le travail sur les meilleurs (cascade)', balance: 'répartit également entre tous (round-robin)', enough: 'qualifié mais le moins sur-qualifié → réserve les experts (distance + temps pris en compte)' })[suggestDlg.strategy] }} + +
+
+
Priorité : distancespécialité
+ +
0 = plus court chemin d'abord · haut = garde les spécialistes sur leur métier
+
+
+ +
repli sans coords
+
+
+ +
budget par jour
+
+
⚡ Les jobs urgents/élevés sont priorisés (jamais laissés de côté) ; basse priorité lâchée en premier si la capacité manque.
+
Répartit sur : {{ suggestWindowLabel }} — défaut aujourd'hui + demain ; filtre par date (chips de la liste) pour cibler d'autres jours. Les jobs datés au-delà sont laissés pour plus tard.
Rapide : @@ -3887,7 +3904,11 @@ function selectAllSector (city) { if (assignPanel.showMap) refreshAssignMap() } // ── D : SUGGESTION de répartition (glouton proximité + charge + compétence), REVUE en surcouche avant d'appliquer ── -const suggestDlg = reactive({ open: false, mode: 'config', plan: [], building: false, applying: false, fromSel: false, techSel: {}, makeShifts: true, strategy: 'smart', view: 'list' }) // strategy: smart|best|balance|enough · view: list|map +const suggestDlg = reactive({ open: false, mode: 'config', plan: [], building: false, applying: false, fromSel: false, techSel: {}, makeShifts: true, strategy: 'smart', view: 'list' }) // strategy: optimize|smart|best|balance|enough · view: list|map +// Réglages de l'optimiseur VRP (curseur distance↔spécialité, vitesse repli, budget de calcul) — persistés localement, ajustables sans redéploiement. +const solverOpts = reactive({ rankWeight: 2, speedKmh: 45, maxSeconds: 8 }) +try { const s = JSON.parse(localStorage.getItem('ops.solverOpts') || 'null'); if (s && typeof s === 'object') Object.assign(solverOpts, s) } catch (e) { /* défauts */ } +watch(solverOpts, v => { try { localStorage.setItem('ops.solverOpts', JSON.stringify(v)) } catch (e) {} }, { deep: true }) function jobDur (j) { return (j && (j.est_min ? j.est_min / 60 : Number(j.duration_h || 1))) || 1 } // Construit un plan job→tech×jour SANS rien écrire (proposition). Chaque job garde sa date si elle est dans la fenêtre. function buildSuggestion () { @@ -4088,11 +4109,14 @@ async function optimizeSuggestion () { ;(byDay[e.iso] = byDay[e.iso] || []).push(e) } const selTechs = (visibleTechs.value || []).filter(t => suggestDlg.techSel[t.id]) + // Priorité du job → « priority_boost » (pénalité de drop) : URGENT jamais laissé de côté ; BASSE lâchée en 1er si la capacité manque. + const poolPr = Object.fromEntries((assignPanel.jobs || []).map(j => [j.name, String(j.priority || '').toLowerCase()])) + const prBoost = p => ({ urgent: 400000, high: 400000, 'élevée': 400000, medium: 0, moyenne: 0, normal: 0, low: -60000, basse: -60000 }[p] ?? 0) const out = [...kept] for (const iso of Object.keys(byDay)) { const entries = byDay[iso] const byName = Object.fromEntries(entries.map(e => [e.jobName, e])) - const jobs = entries.map(e => ({ id: e.jobName, lat: e.lat, lon: e.lon, service_min: Math.max(5, Math.round((e.dur || 1) * 60)), skill: e.skill || '' })) + const jobs = entries.map(e => ({ id: e.jobName, lat: e.lat, lon: e.lon, service_min: Math.max(5, Math.round((e.dur || 1) * 60)), skill: e.skill || '', priority_boost: prBoost(poolPr[e.jobName]) })) const vehicles = [] for (const t of selTechs) { // Fenêtre = quart réel ; SINON quart 8-16 supposé (créé au Publier). Jamais de quart auto le week-end → pas de véhicule. @@ -4103,7 +4127,7 @@ async function optimizeSuggestion () { } if (!vehicles.length) { out.push(...entries); continue } // aucun tech éligible (ex. week-end sans quart) → garder l'assignation greedy let res - try { res = await roster.optimizeRoutes({ jobs, vehicles, max_seconds: 8, rank_weight: 2 }) } catch (e) { out.push(...entries); continue } // rank_weight faible = DISTANCE prioritaire, spécialité = simple départage + try { res = await roster.optimizeRoutes({ jobs, vehicles, max_seconds: solverOpts.maxSeconds, rank_weight: solverOpts.rankWeight, speed_kmh: solverOpts.speedKmh }) } catch (e) { out.push(...entries); continue } // réglages ajustables (curseur distance↔spécialité, vitesse, budget calcul) if (!res || res.status !== 'OK') { out.push(...entries); continue } // solveur absent/erreur/infaisable → on GARDE l'assignation greedy (ne jamais perdre de jobs) const placed = new Set() for (const rt of (res.routes || [])) {