diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue
index 07b3e9b..1000488 100644
--- a/apps/ops/src/pages/PlanificationPage.vue
+++ b/apps/ops/src/pages/PlanificationPage.vue
@@ -676,6 +676,7 @@
{{ j.detail }}
+
{{ j.detail }}
{{ j.subject }}
{{ fmtHM(packedDay[i].startMin) }}–{{ fmtHM(packedDay[i].endMin) }} · 🔒 RDV fixe · {{ j.customer }}
@@ -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 => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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(`
${_esc(p.label)}. ${_esc(p.subject)}
🕒 ${_esc(p.time)}${p.customer ? '
👤 ' + _esc(p.customer) : ''}
`)
+ .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); }