diff --git a/apps/ops/src/api/roster.js b/apps/ops/src/api/roster.js index f1f37d9..f274219 100644 --- a/apps/ops/src/api/roster.js +++ b/apps/ops/src/api/roster.js @@ -236,3 +236,7 @@ export const geofenceStates = (names) => jget('/roster/geofence-states?jobs=' + export const listTraccarDevices = () => jget('/traccar/devices') // Associer / changer (device_id) ou dissocier ('') l'appareil Traccar d'un tech — INLINE depuis Planif. export const setTechTraccarDevice = (id, deviceId) => jpost('/roster/technician/' + encodeURIComponent(id) + '/traccar-device', { device_id: deviceId }) +// Roster appareils GPS enrichi (device + tech(s) assigné(s) + non-associé + dernier signal) — vue « appareil → tech ». +export const listTraccarDevicesRoster = () => jget('/roster/traccar-devices') +// Assigner un appareil → un tech (SENS INVERSE) ; libère l'ancien détenteur. tech_id vide = dissocier de tous. +export const assignTraccarDevice = (deviceId, techId) => jpost('/roster/traccar-device-assign', { device_id: deviceId, tech_id: techId || '' }) diff --git a/apps/ops/src/components/shared/GpsDevicesDialog.vue b/apps/ops/src/components/shared/GpsDevicesDialog.vue new file mode 100644 index 0000000..dab8a65 --- /dev/null +++ b/apps/ops/src/components/shared/GpsDevicesDialog.vue @@ -0,0 +1,101 @@ + + + + diff --git a/services/targo-hub/lib/roster.js b/services/targo-hub/lib/roster.js index c4d2f6b..34fcaa7 100644 --- a/services/targo-hub/lib/roster.js +++ b/services/targo-hub/lib/roster.js @@ -2239,6 +2239,28 @@ async function handle (req, res, method, path, url) { const u = new URL(req.url, 'http://localhost') return json(res, 200, await require('./geofence').reconcileFromTrack({ date: u.searchParams.get('date') || undefined, dryRun: u.searchParams.get('dry') === '1' })) } + // Roster des appareils GPS Traccar ENRICHI (appareil + tech(s) assigné(s) + non-associé + dernier signal) → vue « appareil → tech ». + if (path === '/roster/traccar-devices' && method === 'GET') { + const [devs, techs] = await Promise.all([require('./traccar').getDevices().catch(() => []), fetchTechnicians().catch(() => [])]) + const byDev = new Map() // deviceId(String) → [{id,name}] (peut être >1 = doublon à signaler) + for (const t of (techs || [])) { const d = t.traccar_device_id; if (!d) continue; const k = String(d); if (!byDev.has(k)) byDev.set(k, []); byDev.get(k).push({ id: t.id, name: t.name }) } + const out = (devs || []).map(d => { const holders = byDev.get(String(d.id)) || []; return { id: d.id, name: d.name || ('#' + d.id), unique_id: d.uniqueId || '', status: d.status || 'unknown', last_update: d.lastUpdate || null, assigned: holders, unassigned: holders.length === 0, duplicate: holders.length > 1 } }) + return json(res, 200, { devices: out, count: out.length, unassigned: out.filter(x => x.unassigned).length, duplicates: out.filter(x => x.duplicate).length }) + } + // Assigner un appareil → un tech (SENS INVERSE). Libère d'abord tout AUTRE tech détenant l'appareil (le champ vit sur le tech → + // sans ça 2 techs pointeraient le même device). tech_id vide = simplement dissocier l'appareil de tous. + if (path === '/roster/traccar-device-assign' && method === 'POST') { + const b = await parseBody(req); const deviceId = String(b.device_id == null ? '' : b.device_id).trim(); const techId = String(b.tech_id || '').trim() + if (!deviceId) return json(res, 400, { ok: false, error: 'device_id requis' }) + const techs = await fetchTechnicians().catch(() => []) + const holders = (techs || []).filter(t => String(t.traccar_device_id) === deviceId && t.id !== techId) + const released = [] + for (const h of holders) { try { const nm = await resolveTechName(h.id); if (nm) { await retryWrite(() => erp.update('Dispatch Technician', nm, { traccar_device_id: '' })); released.push(h.id) } } catch (e) {} } + let assigned = null + if (techId) { const nm = await resolveTechName(techId); if (!nm) return json(res, 404, { ok: false, error: 'tech introuvable: ' + techId }); const r = await retryWrite(() => erp.update('Dispatch Technician', nm, { traccar_device_id: deviceId })); if (!(r && r.ok)) return json(res, 500, { ok: false, error: (r && r.error) || 'écriture échouée' }); assigned = techId } + invalidateTechCache() + return json(res, 200, { ok: true, device_id: deviceId, tech_id: techId || null, assigned, released }) + } // Associer / changer (ou retirer) l'appareil Traccar d'un tech (GPS live + tracé) — éditable INLINE depuis Planif. // device_id = id numérique Traccar (ou '' pour dissocier). const mTrk = path.match(/^\/roster\/technician\/(.+)\/traccar-device$/)