feat(dispatch): review job-move menu ranks techs by proximity + occupancy
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) <noreply@anthropic.com>
This commit is contained in:
parent
f5a7547c1f
commit
2566976191
|
|
@ -1080,13 +1080,13 @@
|
|||
<span class="jt-chip" :class="{ on: jobTimeOf(e.jobName).ampm === 'pm' }" @click="toggleJobAmpm(e.jobName, 'pm')"><q-tooltip>Après-midi (12-16) — Ré-optimiser pour appliquer</q-tooltip>PM</span>
|
||||
<span class="jt-chip jt-early" :class="{ on: jobTimeOf(e.jobName).early }" @click="toggleJobEarly(e.jobName)"><q-tooltip>Urgent — servir en DÉBUT de journée (Ré-optimiser)</q-tooltip>⚡</span>
|
||||
<q-space />
|
||||
<q-btn flat dense round size="sm" icon="swap_horiz" color="grey-7"><q-tooltip>Déplacer vers un autre tech</q-tooltip>
|
||||
<q-menu anchor="bottom right" self="top right"><q-list dense style="min-width:220px;max-height:42vh;overflow:auto">
|
||||
<q-item-label v-if="e.skill" header class="q-py-xs">Compétence « {{ e.skill }} »</q-item-label>
|
||||
<q-item v-for="t in capableTechsFirst([e.skill])" :key="t.id" clickable v-close-popup @click="moveSuggestEntry(e, t.id, t.name)" :active="t.id === e.techId" :disable="!covers(t, [e.skill]) && anyCovers([e.skill])">
|
||||
<q-item-section avatar style="min-width:30px"><q-avatar size="22px" :color="covers(t, [e.skill]) ? 'blue-grey-1' : 'grey-3'" text-color="blue-grey-8" style="font-size:9px;font-weight:700">{{ initials(t.name) }}</q-avatar></q-item-section>
|
||||
<q-item-section>{{ t.name }}</q-item-section>
|
||||
<q-item-section side v-if="!covers(t, [e.skill])"><q-icon name="block" color="grey-5" size="15px"><q-tooltip>Sans la compétence</q-tooltip></q-icon></q-item-section>
|
||||
<q-btn flat dense round size="sm" icon="swap_horiz" color="grey-7"><q-tooltip>Déplacer vers un autre tech (classés par proximité + charge)</q-tooltip>
|
||||
<q-menu anchor="bottom right" self="top right"><q-list dense style="min-width:250px;max-height:46vh;overflow:auto">
|
||||
<q-item-label header class="q-py-xs">Techs par <b>proximité</b> + <b>charge</b><span v-if="e.skill" class="text-grey-6"> · {{ e.skill }}</span></q-item-label>
|
||||
<q-item v-for="r in techsForEntry(e)" :key="r.id" clickable v-close-popup @click="moveSuggestEntry(e, r.id, r.name)" :active="r.id === e.techId" :disable="!r.capable && anyCovers([e.skill])">
|
||||
<q-item-section avatar style="min-width:30px"><q-avatar size="22px" :color="r.capable ? 'blue-grey-1' : 'grey-3'" text-color="blue-grey-8" style="font-size:9px;font-weight:700">{{ initials(r.name) }}</q-avatar></q-item-section>
|
||||
<q-item-section>{{ r.name }}<div class="text-grey-6" style="font-size:10px">{{ r.distKm != null ? '📍 ≈' + (Math.round(r.distKm * 10) / 10) + ' km' : 'sans domicile' }} · {{ r.h }}/{{ r.cap }}h<span v-if="r.noShift"> · sans quart</span></div></q-item-section>
|
||||
<q-item-section side v-if="!r.capable"><q-icon name="block" color="grey-5" size="15px"><q-tooltip>Sans la compétence</q-tooltip></q-icon></q-item-section>
|
||||
</q-item>
|
||||
</q-list></q-menu>
|
||||
</q-btn>
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user