From 2566976191118a5c3ce919cfd069e7eb977ecc07 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 2 Jul 2026 15:05:05 -0400 Subject: [PATCH] feat(dispatch): review job-move menu ranks techs by proximity + occupancy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each job's move menu in the optimize review now reuses techsForJob() (capable → with-shift → nearest → least-loaded) instead of alphabetical, showing per tech: distance (home→job km) and load (h/cap). Lets you drop a "non casé" job onto the best nearby, least-busy capable tech — the manual version of "move a job to free a tech's capacity". Works on placeholder/unassigned entries too (same row). Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/pages/PlanificationPage.vue | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index 734943f..ee14949 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -1080,13 +1080,13 @@ Après-midi (12-16) — Ré-optimiser pour appliquerPM Urgent — servir en DÉBUT de journée (Ré-optimiser) - Déplacer vers un autre tech - - Compétence « {{ e.skill }} » - - {{ initials(t.name) }} - {{ t.name }} - Sans la compétence + Déplacer vers un autre tech (classés par proximité + charge) + + Techs par proximité + charge · {{ e.skill }} + + {{ initials(r.name) }} + {{ r.name }}
{{ r.distKm != null ? '📍 ≈' + (Math.round(r.distKm * 10) / 10) + ' km' : 'sans domicile' }} · {{ r.h }}/{{ r.cap }}h · sans quart
+ Sans la compétence
@@ -4227,6 +4227,8 @@ const covers = (t, skills) => (skills || []).filter(Boolean).every(s => (t.skill const anyCovers = (skills) => !((skills || []).filter(Boolean).length) || (visibleTechs.value || []).some(t => covers(t, skills)) // Liste de techs triée CAPABLES d'abord (couvrent la/les compétence(s)), puis alphabétique — pour les menus d'assignation. function capableTechsFirst (skills) { return [...(visibleTechs.value || [])].sort((a, b) => (covers(b, skills) - covers(a, skills)) || String(a.name || '').localeCompare(String(b.name || ''))) } +// Une entrée de la revue → objet job-like pour réutiliser techsForJob (classement proximité + occupation). +function techsForEntry (e) { return techsForJob({ required_skill: e.skill, lat: e.lat, lon: e.lon, scheduled_date: e.iso }) } const techById = computed(() => { const m = {}; for (const t of (visibleTechs.value || [])) m[t.id] = t; return m }) // Réassigne UNE entrée : quand la cible est un VRAI tech, on sort du placeholder + on (re)calcule quart manquant ET compétence. function reassignEntry (e, techId, techName) {