From 2177212a67c2f1bcf1ea3fd3ec32a7c99b021694 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 2 Jul 2026 20:56:27 -0400 Subject: [PATCH] =?UTF-8?q?fix(dispatch):=20GPS=20device=20row=20was=20mis?= =?UTF-8?q?leading=20=E2=80=94=20clear=20linked-device=20chip=20+=20easy?= =?UTF-8?q?=20relink?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tech WAS associated (Stéphane → traccar_device_id "33" = device "24 - Stephane M"), but the q-select's use-input placeholder ("non associé — choisir l'appareil Traccar") kept showing in the filter input even with a value selected, so it read as unassociated. Redesigned the inline GPS row: - Linked: a clear green chip with a 📍 pin + online dot showing the real device name (resolves traccar_device_id → Traccar name), a "Changer" menu (filter + list) to relink to ANOTHER vehicle, and a dissociate button. jdDevice computed; missing id → "#id — introuvable" (not a silent blank). - Not linked: "non associé" + an auto-suggested device (jdDeviceSuggest: Traccar device whose name/uniqueId contains a word of the tech's name) one-click "Associer …", plus a "Choisir" picker menu. Removed the confusing q-select + its filterDevices. Verified: Stéphane's row now shows "📍 24 - Stephane M" (no "non associé"); Changer menu lists all vehicles with a filter. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/pages/PlanificationPage.vue | 59 ++++++++++++++++++------ 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index 9f24492..55283fa 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -1620,19 +1620,29 @@ Choisir le domicile (point de départ) sur la carte Fixer le départ à la position GPS live actuelle du tech (Traccar) - +
- + Appareil GPS : - - - - + + + +
Aucun job ce jour.
@@ -2676,14 +2686,37 @@ async function loadTraccarDevices () { devicesLoading.value = true try { const d = await roster.listTraccarDevices(); traccarDevices.value = Array.isArray(d) ? d : (d && d.devices) || []; _devicesLoaded = true } catch (e) { /* non bloquant */ } finally { devicesLoading.value = false } } -function filterDevices (val, update) { update(() => { deviceFilter.value = (val || '').toLowerCase() }) } const deviceOptions = computed(() => { - const q = deviceFilter.value + const q = deviceFilter.value.toLowerCase().trim() return traccarDevices.value .map(d => ({ label: d.name || d.uniqueId || ('#' + d.id), value: String(d.id), uniqueId: d.uniqueId || '', online: d.status === 'online' })) .filter(o => !q || o.label.toLowerCase().includes(q) || (o.uniqueId && o.uniqueId.toLowerCase().includes(q))) .sort((a, b) => (b.online ? 1 : 0) - (a.online ? 1 : 0) || a.label.localeCompare(b.label)) }) +// Appareil ACTUELLEMENT relié au tech de l'éditeur (résout l'id → nom réel, ex. « 24 - Stephane M »). missing = id enregistré mais introuvable dans Traccar. +const jdDevice = computed(() => { + const id = dayEditor.tech && dayEditor.tech.traccar_device_id + if (!id) return null + const d = traccarDevices.value.find(x => String(x.id) === String(id)) + if (d) return { value: String(d.id), label: d.name || d.uniqueId || ('#' + d.id), online: d.status === 'online', uniqueId: d.uniqueId || '' } + return { value: String(id), label: devicesLoading.value ? ('#' + id + '…') : ('#' + id + ' — introuvable'), online: false, missing: !devicesLoading.value } +}) +// Suggestion AUTO quand non relié : appareil dont le nom/uniqueId contient un mot du nom du tech (≥3 lettres). Facilite « relier ». +const _normDev = (s) => String(s || '').toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '') +const jdDeviceSuggest = computed(() => { + if (jdDevice.value) return null + const t = dayEditor.tech; if (!t) return null + const parts = _normDev(t.name).split(/[^a-z]+/).filter(p => p.length >= 3) + if (!parts.length) return null + let best = null + for (const d of traccarDevices.value) { + const dn = _normDev(d.name) + ' ' + _normDev(d.uniqueId) + const hit = parts.filter(p => dn.includes(p)).length + if (hit > (best ? best.hit : 0)) best = { hit, d } + } + if (best && best.hit >= 1) { const d = best.d; return { value: String(d.id), label: d.name || ('#' + d.id), online: d.status === 'online' } } + return null +}) async function onPickDevice (tech, deviceId) { if (!tech || !tech.id) return const prev = tech.traccar_device_id || ''; const v = deviceId == null ? '' : String(deviceId)