import { apiGet } from '../api.js'; import { hasValidCoordinates, launchDirections, normalizeCoordinate, openExternalMap } from '../navigation.js'; import { pushLocalNotification } from '../notifications.js'; const PROXIMITY_RADIUS_METERS = 5000; const REQUIRED_GPS_ACCURACY_METERS = 4; export async function renderProximity(root) { root.innerHTML = `
proximité agent
Clients FTTH proches

Consultez les clients déjà traités, identifiez ceux à proximité et démarrez un itinéraire terrain immédiatement.

Recherche de votre position terrain…
Chargement des clients de proximité…
`; const statusBox = root.querySelector('#nearbyStatusBox'); const listBox = root.querySelector('#nearbyList'); async function load() { listBox.innerHTML = '
Chargement des clients de proximité…
'; const [response, currentPosition] = await Promise.all([ apiGet('/api/mobile/ftth/nearby').catch(() => null), getCurrentPosition().catch(() => null), ]); const clients = Array.isArray(response?.clients) ? response.clients : []; const sortedClients = sortClientsByDistance(clients, currentPosition); const visibleClients = currentPosition ? sortedClients.filter((client) => client.isNearby) : sortedClients; const hiddenFarCount = currentPosition ? sortedClients.filter((client) => client.distanceMeters !== null && !client.isNearby).length : 0; if (currentPosition) { statusBox.className = 'mobile-route-status tone-success'; statusBox.textContent = hiddenFarCount > 0 ? `Position détectée: ${formatDistance(currentPosition.accuracy || 0)} de précision environ. ${visibleClients.length} client(s) dans un rayon de ${formatDistance(PROXIMITY_RADIUS_METERS)}. ${hiddenFarCount} client(s) trop éloigné(s) masqué(s).` : `Position détectée: ${formatDistance(currentPosition.accuracy || 0)} de précision environ. ${visibleClients.length} client(s) dans un rayon de ${formatDistance(PROXIMITY_RADIUS_METERS)}.`; } else { statusBox.className = 'mobile-route-status tone-warning'; statusBox.textContent = `Position indisponible ou précision GPS insuffisante. Une précision de ${formatDistance(REQUIRED_GPS_ACCURACY_METERS)} est requise.`; } if (!visibleClients.length) { listBox.innerHTML = `

Aucun client proche exploitable.

${currentPosition ? `Aucun client n’a été trouvé dans un rayon de ${formatDistance(PROXIMITY_RADIUS_METERS)} autour de votre position actuelle.` : `Les clients apparaîtront ici dès qu’une position avec une précision de ${formatDistance(REQUIRED_GPS_ACCURACY_METERS)} ou mieux sera disponible.`}

`; return; } listBox.innerHTML = visibleClients.map((client) => renderClientCard(client)).join(''); listBox.querySelectorAll('.js-proximity-map').forEach((button) => { button.addEventListener('click', () => { openExternalMap( button.getAttribute('data-lat'), button.getAttribute('data-lng'), button.getAttribute('data-label') || 'Client FTTH' ); }); }); listBox.querySelectorAll('.js-proximity-route').forEach((button) => { button.addEventListener('click', () => { const clientId = Number(button.getAttribute('data-id') || 0); const lat = button.getAttribute('data-lat'); const lng = button.getAttribute('data-lng'); const label = button.getAttribute('data-label') || 'Client FTTH'; launchDirections(lat, lng, label); pushLocalNotification({ id: `nearby-route-${clientId}-${Date.now()}`, title: 'Itinéraire démarré', body: `Navigation lancée vers ${label}.`, route: '/nearby', icon: 'fa-route', tone: clientId ? 'info' : 'neutral', }, { playSound: true }); }); }); } root.querySelector('#btnRefreshNearby').addEventListener('click', load); await load(); } function renderClientCard(client) { const isMaintenance = Number(client.has_active_maintenance || 0) === 1; const canOpenAssignedMaintenance = isMaintenance && Number(client.assigned_to_me || 0) === 1; const distanceLabel = client.distanceMeters !== null ? `${formatDistance(client.distanceMeters)}${client.isNearby ? ' • à proximité' : ''}` : 'Distance indisponible'; const maintenanceLabel = isMaintenance ? 'Maintenance active' : (Number(client.is_maintenance || 0) === 1 ? 'Historique maintenance' : ''); return `
${escHtml(client.client_name || 'Client FTTH')}
${escHtml(client.ref_code || client.site_name || 'Historique client')}
${maintenanceLabel}
${escHtml(client.client_address || client.site_name || 'Adresse non renseignée')}
${escHtml(distanceLabel)} ${escHtml(client.report_status || 'rapport')} ${Number(client.assigned_to_me || 0) === 1 ? 'Assigné à moi' : ''}
${client.nature_intervention ? `
${escHtml(client.nature_intervention)}
` : ''} ${client.last_reported_at ? `
Dernier traitement: ${escHtml(formatDateTime(client.last_reported_at))}
` : ''}
${canOpenAssignedMaintenance ? ` Ouvrir la maintenance ` : ''}
`; } function getCurrentPosition() { return new Promise((resolve, reject) => { if (!navigator.geolocation) { reject(new Error('geolocation_unavailable')); return; } navigator.geolocation.getCurrentPosition((position) => { const accuracy = Number(position.coords.accuracy || 0); if (!Number.isFinite(accuracy) || accuracy <= 0 || accuracy > REQUIRED_GPS_ACCURACY_METERS) { reject(new Error('geolocation_accuracy_insufficient')); return; } resolve({ lat: Number(position.coords.latitude), lng: Number(position.coords.longitude), accuracy, }); }, reject, { enableHighAccuracy: true, maximumAge: 0, timeout: 20000, }); }); } function sortClientsByDistance(clients, currentPosition) { return [...clients].map((client) => { const clientLat = normalizeCoordinate(client.client_lat); const clientLng = normalizeCoordinate(client.client_lng); const hasClientCoords = hasValidCoordinates(clientLat, clientLng); const distanceMeters = currentPosition ? (hasClientCoords ? calculateDistanceMeters(currentPosition.lat, currentPosition.lng, clientLat, clientLng) : null) : null; return { ...client, client_lat: hasClientCoords ? clientLat : null, client_lng: hasClientCoords ? clientLng : null, distanceMeters, isNearby: !currentPosition || (distanceMeters !== null && distanceMeters <= PROXIMITY_RADIUS_METERS), }; }).filter((client) => hasValidCoordinates(client.client_lat, client.client_lng)).sort((left, right) => { if (Number(left.has_active_maintenance || 0) !== Number(right.has_active_maintenance || 0)) { return Number(right.has_active_maintenance || 0) - Number(left.has_active_maintenance || 0); } if (left.distanceMeters === null && right.distanceMeters === null) return 0; if (left.distanceMeters === null) return 1; if (right.distanceMeters === null) return -1; return left.distanceMeters - right.distanceMeters; }); } function calculateDistanceMeters(startLat, startLng, endLat, endLng) { const earthRadius = 6371000; const toRadians = (value) => (value * Math.PI) / 180; const deltaLat = toRadians(endLat - startLat); const deltaLng = toRadians(endLng - startLng); const a = Math.sin(deltaLat / 2) ** 2 + Math.cos(toRadians(startLat)) * Math.cos(toRadians(endLat)) * Math.sin(deltaLng / 2) ** 2; return earthRadius * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } function formatDistance(value) { const distance = Number(value || 0); if (!Number.isFinite(distance) || distance <= 0) { return '0 m'; } if (distance < 1000) { return `${Math.round(distance)} m`; } return `${(distance / 1000).toFixed(distance < 10000 ? 1 : 0)} km`; } function formatDateTime(value) { const date = new Date(String(value || '').replace(' ', 'T')); return Number.isNaN(date.getTime()) ? String(value || '—') : date.toLocaleString('fr-FR'); } function escHtml(value) { return String(value ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function escAttr(value) { return escHtml(value); }