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:
parent
a6508845a7
commit
5803fb3d0e
|
|
@ -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
|
// « 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.
|
// (assignation + routes) parmi les techs de quart → regroupe les secteurs, respecte compétences + temps sur place.
|
||||||
async function optimizeSuggestion () {
|
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 plan = suggestDlg.plan
|
||||||
const out = plan.filter(e => e.placeholder) // placeholders conservés
|
const byDay = {}; const kept = []
|
||||||
const byDay = {}
|
for (const e of plan) {
|
||||||
for (const e of plan) if (!e.placeholder) (byDay[e.iso] = byDay[e.iso] || []).push(e)
|
// 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 selTechs = (visibleTechs.value || []).filter(t => suggestDlg.techSel[t.id])
|
||||||
|
const out = [...kept]
|
||||||
for (const iso of Object.keys(byDay)) {
|
for (const iso of Object.keys(byDay)) {
|
||||||
const entries = byDay[iso]
|
const entries = byDay[iso]
|
||||||
const byName = Object.fromEntries(entries.map(e => [e.jobName, e]))
|
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 || '' }))
|
||||||
const vehicles = []
|
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 }) }
|
for (const t of selTechs) {
|
||||||
if (!vehicles.length) { out.push(...entries); continue } // aucun tech de quart ce jour → garder l'assignation greedy
|
// 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
|
let res
|
||||||
try { res = await roster.optimizeRoutes({ jobs, vehicles, max_seconds: 8, rank_weight: 6 }) } catch (e) { out.push(...entries); continue }
|
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)
|
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()
|
const placed = new Set()
|
||||||
for (const rt of (res.routes || [])) {
|
for (const rt of (res.routes || [])) {
|
||||||
const t = selTechs.find(x => x.id === rt.vehicle) || { id: rt.vehicle, name: rt.vehicle_name }
|
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 }) }
|
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 }) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user