Planification : minimap du territoire (pins+tracé) dans l'éditeur de journée + retrait du sélecteur priorité

- Minimap Mapbox Static ajoutée sous la timeline : pins numérotés dans l'ordre de tournée + tracé reliant
  les arrêts, ajustée auto au territoire des jobs → on VÉRIFIE d'un coup d'œil que chaque arrêt tombe à la
  bonne adresse (un pin mal placé = coord à corriger via Conformité adresses). Clic → carte interactive (Dispatch).
  Indique « N sans coords (absent de la carte) » le cas échéant. Helper encodePolyline (precision 5) pour le tracé.
- Sélecteur de priorité retiré de chaque ligne (défaut « Moyenne » conservé en donnée, géré au Dispatch) → gain de place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-06-07 16:54:53 -04:00
parent 48c2f53d18
commit 50d877b49f

View File

@ -679,6 +679,11 @@
<div v-for="(b, bi) in dayBlocks()" :key="'k' + bi" class="tl-blk" :style="blockStyle(b, dayOcc() && dayOcc().pct)"></div> <div v-for="(b, bi) in dayBlocks()" :key="'k' + bi" class="tl-blk" :style="blockStyle(b, dayOcc() && dayOcc().pct)"></div>
<span v-for="tk in axisTicks" :key="'t' + tk.h" class="tldlg-tick" :style="{ left: tk.left }">{{ tk.h }}</span> <span v-for="tk in axisTicks" :key="'t' + tk.h" class="tldlg-tick" :style="{ left: tk.left }">{{ tk.h }}</span>
</div> </div>
<!-- minimap : territoire des jobs (pins numérotés dans l'ordre de tournée + tracé) → vérifier les adresses d'un coup d'œil -->
<div v-if="dayMapUrl" class="de-map-wrap" @click="gotoDispatch(dayEditor.tech, dayEditor.day && dayEditor.day.iso)">
<img :src="dayMapUrl" class="de-map" alt="Territoire du jour" loading="lazy" />
<div class="de-map-cap">🗺 Pins = arrêts dans l'ordre · clic = carte interactive (Dispatch)<span v-if="dayNoCoord" class="text-deep-orange-7"> · {{ dayNoCoord }} sans coords (absent de la carte)</span></div>
</div>
<div v-if="!dayEditor.list.length" class="text-grey-6 q-pa-md text-center">Aucun job ce jour.</div> <div v-if="!dayEditor.list.length" class="text-grey-6 q-pa-md text-center">Aucun job ce jour.</div>
<!-- liste éditable : flèches/glisser pour réordonner · durée en minutes · pour retirer --> <!-- liste éditable : flèches/glisser pour réordonner · durée en minutes · pour retirer -->
<template v-for="(j, i) in dayEditor.list" :key="j.name"> <template v-for="(j, i) in dayEditor.list" :key="j.name">
@ -701,9 +706,7 @@
<div class="ellipsis text-grey-6" style="font-size:11px">{{ fmtHM(packedDay[i].startMin) }}{{ fmtHM(packedDay[i].endMin) }}<span v-if="j.locked" class="text-deep-orange-7"> · 🔒 RDV fixe</span><span v-if="j.customer"> · {{ j.customer }}</span></div> <div class="ellipsis text-grey-6" style="font-size:11px">{{ fmtHM(packedDay[i].startMin) }}{{ fmtHM(packedDay[i].endMin) }}<span v-if="j.locked" class="text-deep-orange-7"> · 🔒 RDV fixe</span><span v-if="j.customer"> · {{ j.customer }}</span></div>
</div> </div>
<div class="de-dur"><input type="number" min="5" step="5" :value="jobMinutes(j)" @change="setJobMinutes(j, $event.target.value)" @click.stop @mousedown.stop /><span>min</span></div> <div class="de-dur"><input type="number" min="5" step="5" :value="jobMinutes(j)" @change="setJobMinutes(j, $event.target.value)" @click.stop @mousedown.stop /><span>min</span></div>
<select :value="j.priority" @change="j.priority = $event.target.value" class="de-prio" :style="{ borderColor: prioColor(j.priority) }"> <!-- sélecteur de priorité retiré (défaut « Moyenne » conservé en donnée) gain de place ; priorité gérée au Dispatch -->
<option value="urgent">Urgent</option><option value="high">Élevée</option><option value="medium">Moyenne</option><option value="low">Basse</option>
</select>
<q-btn flat dense round size="sm" :icon="j.locked ? 'lock' : 'lock_open'" :color="j.locked ? 'deep-orange' : 'grey-5'" @click="j.locked = !j.locked"><q-tooltip>{{ j.locked ? 'Heure FIXE (RDV) — verrouillée, non replanifiée' : 'Heure flexible — replanifiée par la tournée' }}</q-tooltip></q-btn> <q-btn flat dense round size="sm" :icon="j.locked ? 'lock' : 'lock_open'" :color="j.locked ? 'deep-orange' : 'grey-5'" @click="j.locked = !j.locked"><q-tooltip>{{ j.locked ? 'Heure FIXE (RDV) — verrouillée, non replanifiée' : 'Heure flexible — replanifiée par la tournée' }}</q-tooltip></q-btn>
<q-btn flat dense round size="sm" icon="close" color="negative" @click="removeFromDay(j)"><q-tooltip>Retirer du tech (retour au pool)</q-tooltip></q-btn> <q-btn flat dense round size="sm" icon="close" color="negative" @click="removeFromDay(j)"><q-tooltip>Retirer du tech (retour au pool)</q-tooltip></q-btn>
</div> </div>
@ -1095,6 +1098,31 @@ const packedDay = computed(() => {
} }
return out return out
}) })
// Encodage polyline (precision 5, deltas entiers) pour le tracé de la minimap Mapbox Static.
function encodePolyline (coords) {
let prevLat = 0, prevLon = 0, out = ''
const enc = (cur, prev) => { let v = cur - prev; v = v < 0 ? ~(v << 1) : (v << 1); let s = ''; while (v >= 0x20) { s += String.fromCharCode((0x20 | (v & 0x1f)) + 63); v >>= 5 } s += String.fromCharCode(v + 63); return s }
for (const [lat, lon] of coords) { const la = Math.round(lat * 1e5); const lo = Math.round(lon * 1e5); out += enc(la, prevLat) + enc(lo, prevLon); prevLat = la; prevLon = lo }
return out
}
// Minimap du jour (Mapbox Static) : pins numérotés dans l'ordre de tournée + tracé, ajustée au territoire (auto).
// permet de VÉRIFIER d'un coup d'œil que chaque arrêt tombe à la bonne adresse (un pin mal placé = coord à corriger).
const hasLL = (j) => j && j.lat != null && j.lon != null && isFinite(+j.lat) && isFinite(+j.lon)
const dayMapUrl = computed(() => {
if (!MAPBOX_TOKEN) return null
const pts = packedDay.value.filter(hasLL).slice(0, 24)
if (!pts.length) return null
const markers = pts.map((j, idx) => {
const hex = (j.skill ? getTagColor(j.skill) : '#1976d2').replace('#', '')
const lbl = idx < 9 ? '-' + (idx + 1) : '' // pin-s : label = 1 caractère (1..9) ; au-delà = pin sans numéro
return `pin-s${lbl}+${hex}(${(+j.lon).toFixed(5)},${(+j.lat).toFixed(5)})`
})
const overlays = []
if (pts.length > 1) overlays.push(`path-2+3b5bdb-0.55(${encodeURIComponent(encodePolyline(pts.map(j => [+j.lat, +j.lon])))})`) // tracé (ordre)
overlays.push(...markers) // marqueurs au-dessus du tracé
return `https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/${overlays.join(',')}/auto/520x200@2x?padding=35&access_token=${MAPBOX_TOKEN}`
})
const dayNoCoord = computed(() => dayEditor.list.filter(j => !hasLL(j)).length)
const dayTotalH = () => Math.round(dayEditor.list.reduce((s, j) => s + (Number(j.dur) || 0), 0) * 10) / 10 const dayTotalH = () => Math.round(dayEditor.list.reduce((s, j) => s + (Number(j.dur) || 0), 0) * 10) / 10
async function removeFromDay (j) { async function removeFromDay (j) {
try { await roster.unassignJobRoster(j.name); dayEditor.list = dayEditor.list.filter(x => x.name !== j.name); await loadWeek(); $q.notify({ type: 'info', message: 'Retiré du tech (retour au pool « à assigner »)', timeout: 2200 }) } catch (e) { err(e) } try { await roster.unassignJobRoster(j.name); dayEditor.list = dayEditor.list.filter(x => x.name !== j.name); await loadWeek(); $q.notify({ type: 'info', message: 'Retiré du tech (retour au pool « à assigner »)', timeout: 2200 }) } catch (e) { err(e) }
@ -1838,6 +1866,11 @@ tr.res-hidden .hide-eye { opacity: 1; }
.de-row:hover { background: #f7f5fc; } .de-row:hover { background: #f7f5fc; }
.de-ord { font-size: 12px; font-weight: 700; color: #607d8b; min-width: 16px; text-align: center; } .de-ord { font-size: 12px; font-weight: 700; color: #607d8b; min-width: 16px; text-align: center; }
.de-dot { width: 11px; height: 11px; border-radius: 3px; flex: 0 0 auto; } .de-dot { width: 11px; height: 11px; border-radius: 3px; flex: 0 0 auto; }
/* minimap du jour (territoire des arrêts) */
.de-map-wrap { margin: 8px 0 4px; cursor: pointer; border-radius: 8px; overflow: hidden; border: 1px solid #e0e0e0; }
.de-map-wrap:hover { border-color: #3b5bdb; }
.de-map { display: block; width: 100%; height: auto; }
.de-map-cap { font-size: 10px; color: #777; padding: 3px 6px; background: #fafafa; border-top: 1px solid #eee; }
.de-prio { font-size: 11px; border: 1px solid #ccc; border-left-width: 4px; border-radius: 4px; padding: 2px 4px; background: #fff; } .de-prio { font-size: 11px; border: 1px solid #ccc; border-left-width: 4px; border-radius: 4px; padding: 2px 4px; background: #fff; }
.de-dur { display: flex; align-items: center; gap: 2px; font-size: 10px; color: #888; } .de-dur { display: flex; align-items: center; gap: 2px; font-size: 10px; color: #888; }
.de-dur input { width: 46px; font-size: 11px; text-align: right; border: 1px solid #cfc4e8; border-radius: 4px; padding: 2px 3px; } .de-dur input { width: 46px; font-size: 11px; text-align: right; border: 1px solid #cfc4e8; border-radius: 4px; padding: 2px 3px; }