diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue
index bf5f2ec..209cf2c 100644
--- a/apps/ops/src/pages/PlanificationPage.vue
+++ b/apps/ops/src/pages/PlanificationPage.vue
@@ -978,22 +978,13 @@
{{ ({ 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é : distance ↔ spécialité
-
-
0 = plus court chemin d'abord · haut = garde les spécialistes sur leur métier
-
-
-
-
⚡ 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.
+
+
+ Coûts :
+ Coût par cran d'ordre de compétence — 0 = distance seule ; élevé = garde les spécialistes sur leur métier
+ Coût par minute au-delà de 100 % du quart — élevé = étale davantage avant de dépasser (plafond 120 %)
+ Vitesse de repli (nœuds sans coordonnées)
+ Budget de calcul par jour
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.
@@ -3936,7 +3927,7 @@ function setEntryPriority (name, lvl) { // écrit la priorité ERPNext du job (l
patchJob(j, { priority: lvl || 'medium' }, 'Priorité : ' + priorityMeta(lvl).label)
}
// 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 })
+const solverOpts = reactive({ rankWeight: 2, overtimeCoef: 20, speedKmh: 45, maxSeconds: 8 }) // un COÛT par critère (pas de slider)
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 }
@@ -4171,7 +4162,7 @@ async function optimizeSuggestion () {
}
if (!vehicles.length) { unassigned.push(...entries); continue } // aucun tech éligible ce jour → non assignés
let res
- try { res = await roster.optimizeRoutes({ jobs, vehicles, max_seconds: solverOpts.maxSeconds, rank_weight: solverOpts.rankWeight, speed_kmh: solverOpts.speedKmh }) } catch (e) { unassigned.push(...entries); continue }
+ try { res = await roster.optimizeRoutes({ jobs, vehicles, max_seconds: solverOpts.maxSeconds, rank_weight: solverOpts.rankWeight, speed_kmh: solverOpts.speedKmh, overtime_coef: solverOpts.overtimeCoef }) } catch (e) { unassigned.push(...entries); continue }
if (!res || res.status !== 'OK') { unassigned.push(...entries); continue } // solveur absent/erreur → non assignés (ne jamais perdre de jobs)
const placed = new Set()
for (const rt of (res.routes || [])) {