'use strict' const cfg = require('./config') const { log, json, parseBody } = require('./helpers') const erp = require('./erp') // Prefer bearer token (revocable, no password in env); fall back to Basic auth for legacy. const authHeader = cfg.TRACCAR_TOKEN ? 'Bearer ' + cfg.TRACCAR_TOKEN : 'Basic ' + Buffer.from(cfg.TRACCAR_USER + ':' + cfg.TRACCAR_PASS).toString('base64') async function traccarFetch (path) { const res = await fetch(cfg.TRACCAR_URL + path, { headers: { Authorization: authHeader, Accept: 'application/json' }, }) if (!res.ok) throw new Error(`Traccar ${res.status}: ${path}`) return res.json() } // Variante RAW (texte) — pour GPX/KML (Accept différent, pas de parse JSON). async function traccarRaw (path, accept) { const res = await fetch(cfg.TRACCAR_URL + path, { headers: { Authorization: authHeader, Accept: accept } }) if (!res.ok) throw new Error(`Traccar ${res.status}: ${path}`) return res.text() } // Résout l'appareil Traccar depuis ?device= OU ?tech= (technician_id puis docname Dispatch Technician). async function deviceFor (url) { let device = url.searchParams.get('device'); const tech = url.searchParams.get('tech') if (!device && tech) { let rows = await erp.list('Dispatch Technician', { filters: [['technician_id', '=', tech]], fields: ['traccar_device_id'], limit: 1 }) if (!rows.length) rows = await erp.list('Dispatch Technician', { filters: [['name', '=', tech]], fields: ['traccar_device_id'], limit: 1 }) device = rows.length ? rows[0].traccar_device_id : null } return device } // Cache devices for 60s (they rarely change) let _devCache = null, _devCacheTs = 0 async function getDevices () { if (_devCache && Date.now() - _devCacheTs < 60000) return _devCache _devCache = await traccarFetch('/api/devices?all=true') _devCacheTs = Date.now() return _devCache } async function getPositions (deviceIds) { if (!deviceIds?.length) return [] const results = await Promise.allSettled( deviceIds.map(id => traccarFetch('/api/positions?deviceId=' + id)) ) return results.flatMap(r => r.status === 'fulfilled' ? r.value : []) } async function handle (req, res, method, path) { const sub = path.replace('/traccar', '').replace(/^\//, '') if (sub === 'devices' && method === 'GET') { try { return json(res, 200, await getDevices()) } catch (e) { log('Traccar devices error:', e.message) return json(res, 502, { error: 'Traccar unavailable' }) } } if (sub === 'positions' && method === 'GET') { try { const url = new URL(req.url, 'http://localhost') const ids = (url.searchParams.get('deviceIds') || '').split(',').map(Number).filter(Boolean) return json(res, 200, await getPositions(ids)) } catch (e) { log('Traccar positions error:', e.message) return json(res, 502, { error: 'Traccar unavailable' }) } } // Export GPX du parcours d'un tech sur une fenêtre (jour courant) — résout le device depuis Dispatch Technician. if (sub === 'gpx' && method === 'GET') { try { const url = new URL(req.url, 'http://localhost') const device = await deviceFor(url) if (!device) return json(res, 404, { error: 'Aucun appareil GPS associé à ce technicien' }) const from = url.searchParams.get('from'); const to = url.searchParams.get('to') if (!from || !to) return json(res, 400, { error: 'from + to (ISO 8601) requis' }) const gpx = await traccarRaw(`/api/positions?deviceId=${encodeURIComponent(device)}&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`, 'application/gpx+xml') res.writeHead(200, { 'Content-Type': 'application/gpx+xml; charset=utf-8', 'Content-Disposition': `attachment; filename="parcours-${device}-${String(from).slice(0, 10)}.gpx"` }) return res.end(gpx) } catch (e) { log('Traccar gpx error:', e.message); return json(res, 502, { error: 'Traccar GPX indisponible' }) } } // Tracé GPS (breadcrumb) d'un tech sur UNE journée → coords [lon,lat] (JSON léger) pour superposer sur la carte. // IMPORTANT : exiger une plage EXACTE (1 jour) — sinon Traccar renvoie un volume énorme et lent. if (sub === 'track' && method === 'GET') { try { const url = new URL(req.url, 'http://localhost') const device = await deviceFor(url) if (!device) return json(res, 404, { error: 'Aucun appareil GPS associé à ce technicien' }) const from = url.searchParams.get('from'); const to = url.searchParams.get('to') if (!from || !to) return json(res, 400, { error: 'from + to (ISO 8601) requis' }) const pts = await traccarFetch(`/api/positions?deviceId=${encodeURIComponent(device)}&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`) const coords = (Array.isArray(pts) ? pts : []).filter(p => p.latitude != null && p.longitude != null).map(p => [p.longitude, p.latitude]) return json(res, 200, { device, count: coords.length, coords }) } catch (e) { log('Traccar track error:', e.message); return json(res, 502, { error: 'Traccar track indisponible' }) } } // Proxy any other Traccar API path (fallback) if (method === 'GET') { try { return json(res, 200, await traccarFetch('/api/' + sub)) } catch (e) { return json(res, 502, { error: e.message }) } } json(res, 404, { error: 'Traccar endpoint not found' }) } module.exports = { handle, getDevices, getPositions }