feat(map): véhicules Traccar libellés par initiales du TECH, repli sur le device

Demande user : afficher les véhicules Traccar avec les initiales du TECHNICIEN assigné,
et ne retomber sur les initiales du device Traccar QUE si aucun tech n'est assigné au véhicule.

- hub /roster/tech-positions : part désormais de TOUS les devices Traccar (pas seulement ceux
  liés à un tech). Pour chaque device localisé : techName si un tech y est assigné (via
  traccar_device_id), sinon deviceName ; renvoie techName + deviceName + name résolu (tech>device).
  Exclut l'île nulle (0,0 = jamais localisé). Live : 23 positions (14 tech, 9 device-only).
- RouteMap : libellé marqueur = initials(techName || deviceName || name) (tech prioritaire).
- PlanificationPage dayLivePositions : les véhicules SANS tech s'affichent aussi (gris),
  filtrés par visibilité seulement s'ils ont un tech.

PlanificationPage.vue = co-édité → DÉPLOYÉ (build) mais NON commité (hunk à isoler).
Vérifié : endpoint live OK ; build spa OK, md5 match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-20 11:44:04 -04:00
parent 08a68016ec
commit 3547f4a680
2 changed files with 26 additions and 10 deletions

View File

@ -16,7 +16,7 @@ import { initials } from 'src/composables/useFormatters' // initiales (source un
const props = defineProps({
routes: { type: Array, default: () => [] },
live: { type: Array, default: () => [] }, // positions GPS LIVE : [{ techId, name|techName, color, lat, lon, time, speed(nœuds) }]
live: { type: Array, default: () => [] }, // positions GPS LIVE : [{ techId, techName, deviceName, name, color, lat, lon, time, speed(nœuds) }] libellé = techName > deviceName
pins: { type: Array, default: () => [] }, // jobs NON assignés à situer : [{ lat, lon, subject, name, color(priorité), city }]
track: { type: Object, default: null }, // tracé GPS RÉEL (Traccar) d'UN tech sur la journée : { coords:[[lon,lat]] } couche orange au 1er plan (vs tournées prévues)
height: { type: String, default: '440px' },
@ -88,7 +88,7 @@ function renderLive (list) {
// pas `wrap` (comme les arrêts translatent .rm-pill, enfant de .rm-mk), sinon on casserait le placement GPS.
const inner = document.createElement('div'); inner.className = 'rm-live-in'
inner.innerHTML = '<span class="rm-live-pulse" style="background:' + color + '"></span>' +
'<span class="rm-live-dot" style="background:' + color + '">' + initials(p.name || p.techName) + '</span>'
'<span class="rm-live-dot" style="background:' + color + '">' + initials(p.techName || p.deviceName || p.name) + '</span>'
wrap.appendChild(inner)
const spd = (p.speed != null && p.speed > 1) ? Math.round(p.speed * 1.852) + ' km/h' : 'arrêté' // Traccar speed = nœuds km/h
const when = ageMin == null ? 'position' : (ageMin <= 1 ? "à l'instant" : 'il y a ' + ageMin + ' min')

View File

@ -2431,17 +2431,33 @@ async function handle (req, res, method, path, url) {
// Positions GPS LIVE de TOUS les techs ayant un appareil Traccar → marqueurs « live » sur la carte des tournées
// (rafraîchi périodiquement côté client). Léger : une requête position par appareil (getPositions parallèle).
if (path === '/roster/tech-positions' && method === 'GET') {
const techs = (await fetchTechnicians()).filter(t => t.traccar_device_id)
if (!techs.length) return json(res, 200, { positions: [] })
const { getPositions } = require('./traccar')
const ids = [...new Set(techs.map(t => parseInt(t.traccar_device_id)).filter(Boolean))]
// Affiche TOUS les véhicules Traccar. Libellé = initiales du TECH assigné au véhicule ; à défaut (aucun tech
// assigné à ce device) → nom du device Traccar. On renvoie techName + deviceName + `name` résolu (tech > device).
const { getPositions, getDevices } = require('./traccar')
let devices = []
try { devices = await getDevices() } catch (e) { /* Traccar injoignable → devices vide */ }
const techs = await fetchTechnicians()
const techByDev = {}; for (const t of techs) { const d = parseInt(t.traccar_device_id); if (d) techByDev[d] = t } // device → tech assigné
const devById = {}; for (const d of (devices || [])) { const id = parseInt(d.id); if (id) devById[id] = d }
const ids = Object.keys(devById).map(Number)
if (!ids.length) return json(res, 200, { positions: [] })
let positions = []
try { positions = await getPositions(ids) } catch (e) { return json(res, 200, { positions: [], error: e.message }) }
const byDev = {}; for (const p of positions) if (p && p.deviceId != null) byDev[p.deviceId] = p
const out = []
for (const t of techs) {
const p = byDev[parseInt(t.traccar_device_id)]
if (p && p.latitude != null) out.push({ techId: t.id, techName: t.name, lat: p.latitude, lon: p.longitude, time: p.fixTime || p.deviceTime || p.serverTime || null, speed: p.speed || 0 })
for (const p of positions) {
if (!p || p.deviceId == null || p.latitude == null) continue
if (Math.abs(+p.latitude) < 0.01 && Math.abs(+p.longitude) < 0.01) continue // île nulle (0,0) = position invalide (device jamais localisé) → exclu
const t = techByDev[parseInt(p.deviceId)]
const dev = devById[parseInt(p.deviceId)]
const deviceName = (dev && dev.name) || ''
out.push({
deviceId: p.deviceId,
techId: t ? t.id : null,
techName: t ? t.name : null, // initiales du tech si un tech est assigné au véhicule
deviceName, // repli : nom du device Traccar (aucun tech assigné)
name: t ? t.name : deviceName, // libellé résolu : tech > device
lat: p.latitude, lon: p.longitude, time: p.fixTime || p.deviceTime || p.serverTime || null, speed: p.speed || 0,
})
}
return json(res, 200, { positions: out })
}