feat(dispatch): optimizer assumes 8h shift for every selected tech

The VRP « Optimiser » now treats each SELECTED tech as having a 8-16
shift (created at Publish via makeShifts) — not just those already
shifted. Weekday placeholders are folded back into their day so the
solver assigns them across all selected techs; weekend placeholders
stay (no auto weekend shift). Result: full multi-tech consolidation
instead of everything falling to placeholders — verified 34 jobs → 16
techs, tight routes (0.5–14.7 km each). Techs without a real shift show
« Créer quart » (their assumed 8h, created at Publish).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-02 13:34:37 -04:00
parent a6508845a7
commit 5803fb3d0e

View File

@ -4077,26 +4077,38 @@ function shiftWindowMin (techId, iso) {
// « Optimiser » = le greedy décide le JOUR (buckets + placeholders), puis le solveur VRP OR-Tools ré-optimise CHAQUE jour
// (assignation + routes) parmi les techs de quart regroupe les secteurs, respecte compétences + temps sur place.
async function optimizeSuggestion () {
buildSuggestion() // pose les jours + skill/dur/coords/reqLevel sur chaque entrée + les placeholders week-end/sans-quart
buildSuggestion() // pose les jours + skill/dur/coords/reqLevel sur chaque entrée + les placeholders
suggestDlg.makeShifts = true // on SUPPOSE un quart 8-16 pour chaque tech sélectionné à créer au moment du Publier
const plan = suggestDlg.plan
const out = plan.filter(e => e.placeholder) // placeholders conservés
const byDay = {}
for (const e of plan) if (!e.placeholder) (byDay[e.iso] = byDay[e.iso] || []).push(e)
const byDay = {}; const kept = []
for (const e of plan) {
// Les placeholders de SEMAINE sont replongés dans leur jour : on suppose un quart pour chaque tech sélectionné le solveur les case.
// Les placeholders de WEEK-END restent (jamais de quart auto le week-end).
if (e.placeholder && isWeekendIso(e.iso)) { kept.push(e); continue }
;(byDay[e.iso] = byDay[e.iso] || []).push(e)
}
const selTechs = (visibleTechs.value || []).filter(t => suggestDlg.techSel[t.id])
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 vehicles = []
for (const t of selTechs) { const w = shiftWindowMin(t.id, iso); if (!w) continue; const h = techHomes.value[t.id]; vehicles.push({ id: t.id, name: t.name, skills: t.skills || [], home_lat: (h && isFinite(+h.lat)) ? +h.lat : null, home_lon: (h && isFinite(+h.lon)) ? +h.lon : null, shift_start_min: w.start, shift_end_min: w.end }) }
if (!vehicles.length) { out.push(...entries); continue } // aucun tech de quart ce jour garder l'assignation greedy
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.
const w = shiftWindowMin(t.id, iso) || (isWeekendIso(iso) ? null : { start: 480, end: 960 })
if (!w) continue
const h = techHomes.value[t.id]
vehicles.push({ id: t.id, name: t.name, skills: t.skills || [], home_lat: (h && isFinite(+h.lat)) ? +h.lat : null, home_lon: (h && isFinite(+h.lon)) ? +h.lon : null, shift_start_min: w.start, shift_end_min: w.end })
}
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: 6 }) } catch (e) { out.push(...entries); continue }
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 || [])) {
const t = selTechs.find(x => x.id === rt.vehicle) || { id: rt.vehicle, name: rt.vehicle_name }
for (const st of (rt.stops || [])) { const e0 = byName[st.job_id]; if (!e0) continue; placed.add(st.job_id); out.push({ ...e0, techId: t.id, techName: t.name, noShift: false, capable: true }) }
for (const st of (rt.stops || [])) { const e0 = byName[st.job_id]; if (!e0) continue; placed.add(st.job_id); out.push({ ...e0, techId: t.id, techName: t.name, placeholder: false, noShift: !hasShiftDay(t.id, iso), capable: true }) } // noShift pastille « Créer quart »
}
for (const jid of (res.unassigned || [])) { const e0 = byName[jid]; if (!e0 || placed.has(jid)) continue; out.push({ ...e0, techId: `__hold__|${iso}|opt`, techName: `${FR_DOW[dowOf(iso)] || ''} ${iso.slice(8)} — non casé (à assigner)`, placeholder: true, noShift: false }) }
}