/* Kart-side: vis alle automater med markører, klikk for å pinpoint posisjon. * Bruker Leaflet + OpenStreetMap (ingen API-key). * * For v1 (legacy): kun manuell pin-via-klikk. * For v2: viser live GPS-feed med "manual override"-knapp. */ // HTML-escape for Leaflet-popup: bindPopup tar HTML, og automatnavn er // tenant-styrt tekst (latent lagret XSS — H17, 2026-09-05). const mapEscHtml = (s) => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); const MapPage = ({ lang, tenant, dataVersion }) => { const machines = (tenant && tenant.id !== '__all__') ? DATA.machines.filter(m => m.tenantId === tenant.id) : DATA.machines; const [selected, setSelected] = React.useState(null); const [editingMode, setEditingMode] = React.useState(false); const [error, setError] = React.useState(null); const [refreshKey, setRefreshKey] = React.useState(0); const mapRef = React.useRef(null); const markersRef = React.useRef({}); const tempMarkerRef = React.useRef(null); // Click-handleren registreres KUN én gang, så den må lese editingMode/selected // via refs — ellers fanger den den første rendringens (false/null) verdier og // klikk-for-å-plassere virker aldri. const editingModeRef = React.useRef(false); const selectedRef = React.useRef(null); editingModeRef.current = editingMode; selectedRef.current = selected; // Init kart (kun én gang) React.useEffect(() => { if (mapRef.current || !window.L) return; const map = window.L.map('jgass-map', { center: [60.5, 7.0], // Norge zoom: 6, preferCanvas: true, }); window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap-bidragsytere', }).addTo(map); mapRef.current = map; // Click handler: når i edit-mode, pinpoint posisjon (leser refs, ikke stale closure) map.on('click', (e) => { if (!editingModeRef.current || !selectedRef.current) return; placeTempMarker(e.latlng.lat, e.latlng.lng); }); }, []); const placeTempMarker = (lat, lon) => { if (!mapRef.current) return; if (tempMarkerRef.current) { tempMarkerRef.current.setLatLng([lat, lon]); } else { // Touch-enheter: større markør så den kan treffes/dras med finger const tempD = window.matchMedia('(pointer: coarse)').matches ? 32 : 24; tempMarkerRef.current = window.L.marker([lat, lon], { draggable: true, icon: window.L.divIcon({ html: `
`, className: '', iconSize: [tempD, tempD], iconAnchor: [tempD / 2, tempD / 2], }), }).addTo(mapRef.current); tempMarkerRef.current.on('dragend', (e) => { const ll = e.target.getLatLng(); placeTempMarker(ll.lat, ll.lng); }); } }; // Plot markører når machines endres React.useEffect(() => { if (!mapRef.current) return; // Fjern gamle Object.values(markersRef.current).forEach(m => m.remove()); markersRef.current = {}; const bounds = []; // Touch: 28px markører (18px er for smått å treffe med finger) const mrkD = window.matchMedia('(pointer: coarse)').matches ? 28 : 18; machines.forEach(m => { if (!m.lat || !m.lon) return; const color = m.kind === 'legacy' ? '#fb923c' : m.gps_source === 'gps' ? '#10b981' : '#3b82f6'; const marker = window.L.marker([m.lat, m.lon], { icon: window.L.divIcon({ html: `
`, className: '', iconSize: [mrkD, mrkD], iconAnchor: [mrkD / 2, mrkD / 2], }), title: m.location, }).addTo(mapRef.current); marker.bindPopup( `${mapEscHtml(m.location)}
` + `${mapEscHtml(m.id)}
` + `${m.kind === 'legacy' ? 'v1 (gammel)' : 'v2'}` + (m.gps_source ? ` · ${mapEscHtml(m.gps_source)}` : '') + (m.gpsAddress ? `
${mapEscHtml(m.gpsAddress)}` : '') + `
` ); marker.on('click', () => setSelected(m)); markersRef.current[m.id] = marker; bounds.push([m.lat, m.lon]); }); if (bounds.length > 0 && !selected) { mapRef.current.fitBounds(bounds, { padding: [40, 40], maxZoom: 12 }); } }, [machines, refreshKey]); const saveLocation = async () => { if (!selected || !tempMarkerRef.current) return; setError(null); const ll = tempMarkerRef.current.getLatLng(); try { await window.api.setAutomatLocation(selected.id, { lat: ll.lat, lon: ll.lng, }); // Oppdater i-memory state selected.lat = ll.lat; selected.lon = ll.lng; selected.gps_source = 'manual'; tempMarkerRef.current.remove(); tempMarkerRef.current = null; setEditingMode(false); setRefreshKey(k => k + 1); } catch (e) { setError(e.message); } }; const cancelEdit = () => { if (tempMarkerRef.current) { tempMarkerRef.current.remove(); tempMarkerRef.current = null; } setEditingMode(false); }; const startEdit = () => { if (!selected) return; setEditingMode(true); if (selected.lat && selected.lon) { placeTempMarker(selected.lat, selected.lon); mapRef.current.setView([selected.lat, selected.lon], 14); } }; return (

{lang === 'no' ? 'Kart' : 'Map'}

{machines.length} {lang === 'no' ? 'automater' : 'machines'} {' · '} {machines.filter(m => m.lat).length} {lang === 'no' ? 'med posisjon' : 'with location'}
{!editingMode && selected && ( )} {editingMode && ( <> )}
{editingMode && (
📌 {lang === 'no' ? 'Klikk på kartet for å sette ny posisjon, eller dra den oransje markøren. Trykk "Lagre" når du er ferdig.' : 'Click the map to set a new position.'}
)} {error &&
❌ {error}
} {/* Mobil: .map-layout stabler kart over liste (styles.css) */}

Automater

{machines.map(m => (
{ setSelected(m); if (m.lat && m.lon && mapRef.current) { mapRef.current.setView([m.lat, m.lon], 13); markersRef.current[m.id]?.openPopup(); } }} style={{ padding: 10, marginBottom: 6, borderRadius: 6, cursor: 'pointer', background: selected?.id === m.id ? 'var(--accent-soft)' : 'var(--surface-2)', border: '1px solid ' + (selected?.id === m.id ? 'var(--accent)' : 'var(--border)'), }}>
{m.location}
{m.kind === 'legacy' ? 'v1' : 'v2'} {m.lat && m.lon ? ( {m.gps_source || 'manuell'} ) : ( ingen pos. )}
{m.lat && m.lon && (
{m.lat.toFixed(5)}, {m.lon.toFixed(5)}
)}
))}
); }; Object.assign(window, { MapPage });