Planification éditeur journée : déplacements en pointillés + clic pin = détails + clic ligne = recentre carte

- Barre timeline : l'espace entre 2 jobs (le déplacement) est rendu en POINTILLÉS (au lieu du gris vide),
  tooltip « 🚗 déplacement ». dayTravelSegs() = gaps entre packedDay[i].end et [i+1].start.
- Carte : clic sur un pin → POPUP avec détails du job (n°, sujet, heure, client) ; curseur main au survol.
- Liste : clic sur une ligne → recentre la carte (easeTo zoom 14) sur ce job, en plus d'ouvrir le détail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-06-07 17:50:52 -04:00
parent 307fb4cea5
commit 2665a6a2da

View File

@ -676,6 +676,7 @@
<!-- timeline visuelle (réutilise les blocs colorés par compétence) -->
<div class="tldlg-bar" style="height:20px">
<div v-for="(b, bi) in dayBands()" :key="'b' + bi" class="tl-shift" :class="{ oncall: b.oncall }" :style="{ left: b.left, width: b.width, background: b.bg || undefined }"></div>
<div v-for="(g, gi) in dayTravelSegs()" :key="'tr' + gi" class="tl-travel" :style="pos(g.s, Math.min(g.e, 24))"><q-tooltip class="bg-grey-9" style="font-size:11px">🚗 déplacement</q-tooltip></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>
</div>
@ -701,7 +702,7 @@
<q-icon name="drag_indicator" size="16px" class="text-grey-5" style="cursor:grab" />
<span class="de-ord">{{ i + 1 }}</span>
<span class="de-dot" :style="{ background: j.skill ? getTagColor(j.skill) : prioColor(j.priority) }"></span>
<div class="col" style="min-width:0;cursor:pointer" @click="j.showDetail = !j.showDetail"><q-tooltip v-if="j.detail && !j.showDetail" max-width="360px" class="bg-grey-9" style="white-space:pre-wrap;font-size:11px">{{ j.detail }}</q-tooltip>
<div class="col" style="min-width:0;cursor:pointer" @click="j.showDetail = !j.showDetail; focusDayJob(j)"><q-tooltip v-if="j.detail && !j.showDetail" max-width="360px" class="bg-grey-9" style="white-space:pre-wrap;font-size:11px">{{ j.detail }}</q-tooltip>
<div class="ellipsis text-weight-medium" style="font-size:13px">{{ j.subject }} <q-icon name="info_outline" size="12px" class="text-grey-5" /></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>
@ -1115,9 +1116,17 @@ function ensureMapbox () {
const iv = setInterval(() => { if (window.mapboxgl) { clearInterval(iv); resolve(window.mapboxgl) } }, 150)
})
}
function dayStops () { // arrêts géolocalisés, dans l'ordre de tournée (packedDay)
return packedDay.value.filter(hasLL).map((j, i) => ({ lon: +j.lon, lat: +j.lat, label: String(i + 1), color: j.skill ? getTagColor(j.skill) : '#1976d2' }))
const _esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]))
function dayStops () { // arrêts géolocalisés, dans l'ordre de tournée (packedDay) + infos pour le popup
return packedDay.value.filter(hasLL).map((j, i) => ({
lon: +j.lon, lat: +j.lat, label: String(i + 1), color: j.skill ? getTagColor(j.skill) : '#1976d2',
subject: j.subject || '', customer: j.customer || '', time: fmtHM(j.startMin) + '' + fmtHM(j.endMin),
}))
}
// Segments de DÉPLACEMENT (pointillés) = l'espace entre 2 jobs dans la barre timeline.
const dayTravelSegs = () => { const p = packedDay.value; const out = []; for (let i = 0; i < p.length - 1; i++) { const s = p[i].endMin, e = p[i + 1].startMin; if (e - s > 0.02) out.push({ s, e }) } return out }
// Centre la carte sur un job (clic sur la ligne de la liste).
function focusDayJob (j) { if (_dayMap && hasLL(j)) _dayMap.easeTo({ center: [+j.lon, +j.lat], zoom: 14, duration: 500 }) }
async function initDayMap () {
if (!MAPBOX_TOKEN || !dayMapEl.value || _dayMap) return
const mapboxgl = await ensureMapbox(); if (!mapboxgl || !dayMapEl.value) return
@ -1133,6 +1142,15 @@ async function initDayMap () {
_dayMap.addSource('day-stops', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } })
_dayMap.addLayer({ id: 'day-stops-c', type: 'circle', source: 'day-stops', paint: { 'circle-radius': 13, 'circle-color': ['get', 'color'], 'circle-stroke-width': 2, 'circle-stroke-color': '#fff' } })
_dayMap.addLayer({ id: 'day-stops-l', type: 'symbol', source: 'day-stops', layout: { 'text-field': ['get', 'label'], 'text-font': ['DIN Offc Pro Bold', 'Arial Unicode MS Bold'], 'text-size': 12, 'text-allow-overlap': true }, paint: { 'text-color': '#fff' } })
// Clic sur un pin popup avec les détails du job ; curseur main au survol.
_dayMap.on('mouseenter', 'day-stops-c', () => { _dayMap.getCanvas().style.cursor = 'pointer' })
_dayMap.on('mouseleave', 'day-stops-c', () => { _dayMap.getCanvas().style.cursor = '' })
_dayMap.on('click', 'day-stops-c', (e) => {
const f = e.features[0]; const p = f.properties
new window.mapboxgl.Popup({ offset: 14 }).setLngLat(f.geometry.coordinates)
.setHTML(`<div style="font-size:12px;line-height:1.45"><b>${_esc(p.label)}. ${_esc(p.subject)}</b><br>🕒 ${_esc(p.time)}${p.customer ? '<br>👤 ' + _esc(p.customer) : ''}</div>`)
.addTo(_dayMap)
})
refreshDayMap()
})
}
@ -1925,6 +1943,7 @@ tr.res-hidden .hide-eye { opacity: 1; }
.tl-shift.oncall { background: rgba(255,179,0,.14); border: 1px dashed #f9a825; } /* garde = sur appel hors heures (pointillé ambre) */
.tl-absent { position: absolute; inset: 0; border-radius: 2px; box-sizing: border-box; border: 1px solid #b0b0b0; background: repeating-linear-gradient(45deg, #cfcfcf 0, #cfcfcf 3px, #f0f0f0 3px, #f0f0f0 6px); } /* absent = hachuré gris */
.tl-blk { position: absolute; top: 0; bottom: 0; border-radius: 1px; } /* occupé = barre de statut opaque */
.tl-travel { position: absolute; top: 0; bottom: 0; background-image: repeating-linear-gradient(90deg, #78909c 0 3px, transparent 3px 7px); background-size: 100% 3px; background-repeat: no-repeat; background-position: 0 center; opacity: .85; } /* déplacement = pointillés */
.tod-leg { display: inline-block; width: 46px; height: 9px; border-radius: 2px; vertical-align: middle; background: linear-gradient(to right, hsl(210,45%,91%), hsl(270,45%,83%)); }
.occ-leg { display: inline-block; width: 46px; height: 9px; border-radius: 2px; vertical-align: middle; background: linear-gradient(to right, hsl(122,68%,44%), hsl(32,68%,44%)); }
.leg-absent { display: inline-block; width: 24px; height: 9px; border-radius: 2px; vertical-align: middle; border: 1px solid #b0b0b0; background: repeating-linear-gradient(45deg,#cfcfcf 0,#cfcfcf 3px,#f0f0f0 3px,#f0f0f0 6px); }