diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue
index a213a89..c7d8033 100644
--- a/apps/ops/src/pages/PlanificationPage.vue
+++ b/apps/ops/src/pages/PlanificationPage.vue
@@ -679,6 +679,11 @@
{{ tk.h }}
+
+
+
![Territoire du jour]()
+
🗺 Pins = arrêts dans l'ordre · clic = carte interactive (Dispatch) · ⚠ {{ dayNoCoord }} sans coords (absent de la carte)
+
Aucun job ce jour.
@@ -701,9 +706,7 @@
{{ fmtHM(packedDay[i].startMin) }}–{{ fmtHM(packedDay[i].endMin) }} · 🔒 RDV fixe · {{ j.customer }}
min
-
+
{{ j.locked ? 'Heure FIXE (RDV) — verrouillée, non replanifiée' : 'Heure flexible — replanifiée par la tournée' }}
Retirer du tech (retour au pool)
@@ -1095,6 +1098,31 @@ const packedDay = computed(() => {
}
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
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) }
@@ -1838,6 +1866,11 @@ tr.res-hidden .hide-eye { opacity: 1; }
.de-row:hover { background: #f7f5fc; }
.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; }
+/* 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-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; }