From 5803fb3d0ec4debfacda30f4001e96eb92ff4774 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 2 Jul 2026 13:34:37 -0400 Subject: [PATCH] feat(dispatch): optimizer assumes 8h shift for every selected tech MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- apps/ops/src/pages/PlanificationPage.vue | 26 +++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index c1bcbd9..6435515 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -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 }) } }