From 7b7980a12b8141d427b88402e079e99d69171b77 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 2 Jul 2026 21:02:27 -0400 Subject: [PATCH] =?UTF-8?q?feat(routemap):=20pill=20markers=20=E2=80=94=20?= =?UTF-8?q?job-type=20icon=20+=20sequence=20number=20(best=20practice)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route-stop markers changed from a plain number disc to a rounded- rectangle "pill" carrying BOTH the job-type icon (material-icons) and the sequence number, in the tech's route color with a white border — the standard for markers that show two facts (logistics/route tools). Number stays bold (primary scan key), icon is context. - markerIcon(skill): map-safe resolver returning ONLY material-icons ligatures (skillSym's custom SVGs don't render in a plain ); install→construction, repair→build, tv→live_tv, phone→call, etc. - stops in dayRoutes + suggestRoutes now carry icon; RouteMap renders + number in .rm-pill. Hover fan-out, hover-to- front, and cluster logic unchanged (operate on the pill element). Verified: 43 pills render with glyph+number in tech colors; 8-pin cluster still fans out on hover. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/components/shared/RouteMap.vue | 20 ++++++++++++-------- apps/ops/src/pages/PlanificationPage.vue | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/apps/ops/src/components/shared/RouteMap.vue b/apps/ops/src/components/shared/RouteMap.vue index 1f324e8..efcaf3d 100644 --- a/apps/ops/src/components/shared/RouteMap.vue +++ b/apps/ops/src/components/shared/RouteMap.vue @@ -80,8 +80,11 @@ function renderMarkers (routes) { homeMk.push({ mk, el: wrap, rid: r.id }) } for (const s of (r.stops || [])) { + // Pastille = ROUND-RECT (pill) : icône du type de job + numéro d'ordre (best practice quand 2 infos ; cf. outils de tournée). const wrap = document.createElement('div'); wrap.className = 'rm-mk'; wrap.dataset.rid = String(r.id) - const disc = document.createElement('div'); disc.className = 'rm-disc'; disc.style.background = r.color; disc.textContent = String(s.seq) + const disc = document.createElement('div'); disc.className = 'rm-pill'; disc.style.background = r.color + const ic = /^[a-z0-9_]+$/.test(String(s.icon || '')) ? s.icon : 'place' + disc.innerHTML = '' + ic + '' + s.seq + '' wrap.appendChild(disc) const mk = new mapboxgl.Marker({ element: wrap, anchor: 'center' }).setLngLat([+s.lon, +s.lat]).addTo(map) const rec = { mk, el: wrap, disc, rid: r.id, lngLat: [+s.lon, +s.lat], seq: s.seq, tech: r.name, subject: s.subject || '', stop: s, fan: null, members: null } @@ -211,17 +214,18 @@ watch(() => props.routes, () => { draw() }, { deep: true }) diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index 55283fa..3ab67ca 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -2362,6 +2362,16 @@ function skillIcon (sk) { if (/factur|paiement|compta/.test(s)) return 'receipt_long' return 'bolt' } +// Icône de compétence pour un MARQUEUR carte (DOM, police material-icons) : UNIQUEMENT des ligatures material (pas les SVG +// custom de skillSym) — sinon le glyphe ne s'affiche pas dans un hors q-icon. +function markerIcon (skill) { + const s = String(skill || '').toLowerCase() + if (/t[ée]l[ée]vis|\btv\b|iptv/.test(s)) return 'live_tv' + if (/install/.test(s)) return 'construction' + if (/monteur|poteau|hauteur|nacelle|a[ée]rien|grimp/.test(s)) return 'local_shipping' + const ic = skillIcon(skill) + return (typeof ic === 'string' && /^[a-z0-9_]+$/.test(ic)) ? ic : 'build' +} function onTagsChange (t, items) { const newLabels = (items || []).map(x => typeof x === 'string' ? x : x.tag).filter(Boolean) const removed = (t.skills || []).filter(s => !newLabels.includes(s)) // compétences retirées → vérifier l'impact sur les jobs assignés @@ -4476,7 +4486,7 @@ const suggestRoutes = computed(() => { if (e.lat == null || e.lon == null || !isFinite(+e.lat) || !isFinite(+e.lon) || Math.abs(+e.lat) < 0.01) continue // 0,0 / invalides exclus (sinon la carte dézoome jusqu'au golfe de Guinée) const k = (+e.lat).toFixed(4) + ',' + (+e.lon).toFixed(4) const ex = seen.get(k) - if (ex) { ex.n++; ex.subject = ex.n + ' jobs — ' + ex.first } else { const s = { lat: +e.lat, lon: +e.lon, seq: stops.length + 1, subject: e.subject, first: e.subject, n: 1, name: e.jobName, entry: e }; seen.set(k, s); stops.push(s) } + if (ex) { ex.n++; ex.subject = ex.n + ' jobs — ' + ex.first } else { const s = { lat: +e.lat, lon: +e.lon, seq: stops.length + 1, subject: e.subject, first: e.subject, n: 1, name: e.jobName, entry: e, icon: markerIcon(e.skill) }; seen.set(k, s); stops.push(s) } } if (!stops.length) continue const home = techOrigin(g.techId) // domicile, sinon bureau TARGO @@ -4502,7 +4512,7 @@ const allDayRoutes = computed(() => { // TOUTES les tournées du jour (couleurs if (!jobs.length) continue jobs.sort((a, b) => (a.route_order || 9999) - (b.route_order || 9999) || ((a.start_h ?? 99) - (b.start_h ?? 99))) // ordre de tournée réel const h = techOrigin(t.id) // domicile, sinon bureau TARGO - out.push({ id: t.id, name: t.name, color: routeColor(ci++), home: h ? { lat: h.lat, lon: h.lon } : null, stops: jobs.map((j, i) => ({ lat: +j.lat, lon: +j.lon, seq: i + 1, subject: j.subject, name: j.name, job: j })) }) + out.push({ id: t.id, name: t.name, color: routeColor(ci++), home: h ? { lat: h.lat, lon: h.lon } : null, stops: jobs.map((j, i) => ({ lat: +j.lat, lon: +j.lon, seq: i + 1, subject: j.subject, name: j.name, job: j, icon: markerIcon(j.skill) })) }) } return out })