Files
workorders/web/components/work-orders/wo-map.mjs
T

131 lines
5.3 KiB
JavaScript

class WoMap extends HTMLElement {
#map = null;
#mounted = false;
static get observedAttributes() { return ['lat', 'lng', 'site-name', 'access-notes', 'wo-number']; }
connectedCallback() {
this.style.display = 'block';
// Light DOM — Leaflet needs real DOM, not shadow root
if (!this.querySelector('.wo-map-root')) this.#build();
}
attributeChangedCallback() {
if (this.#mounted) this.#update();
}
#build() {
const lat = parseFloat(this.getAttribute('lat'));
const lng = parseFloat(this.getAttribute('lng'));
const siteName = this.getAttribute('site-name') || 'Work Site';
const accessNotes = this.getAttribute('access-notes') || '';
const woNumber = this.getAttribute('wo-number') || '';
this.innerHTML = `
<style>
.wo-map-root { }
.wo-map-container { height: 280px; border-radius: var(--radius); border: 1px solid var(--border); overflow: hidden; }
.wo-map-actions { display: flex; gap: .5rem; margin-top: .75rem; }
.map-btn { display: inline-flex; align-items: center; gap: .4rem; padding: .45rem .9rem; border-radius: var(--radius); font-size: .813rem; font-weight: 600; cursor: pointer; transition: opacity .15s; text-decoration: none; border: 1px solid var(--border); color: var(--text); background: var(--surface); }
.map-btn:hover { background: var(--surface-2); text-decoration: none; }
.map-btn.primary { background: var(--teal); color: #fff; border-color: transparent; }
.map-btn.primary:hover { opacity: .88; }
.access-notes { margin-top: .875rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); padding: .75rem 1rem; font-size: .875rem; color: var(--text); display: flex; gap: .6rem; align-items: flex-start; }
.access-notes i { color: var(--warning); flex-shrink: 0; margin-top: .1rem; }
.access-text { line-height: 1.5; }
.no-location { text-align: center; padding: 2.5rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text-muted); font-size: .875rem; }
</style>
<div class="wo-map-root">
${lat && lng ? `
<div class="wo-map-container" id="leaflet-map-${this.getAttribute('wo-id') || 'map'}"></div>
<div class="wo-map-actions">
<a class="map-btn primary" id="directions-btn" href="#" target="_blank">
<i data-lucide="navigation" style="width:14px;height:14px"></i> Get Directions
</a>
</div>` : `
<div class="no-location">
<i data-lucide="map-pin-off" style="width:28px;height:28px;margin:0 auto .5rem;opacity:.4;display:block"></i>
No location set for this work order
</div>`}
${accessNotes ? `
<div class="access-notes">
<i data-lucide="key-round" style="width:16px;height:16px"></i>
<div class="access-text">${this.#esc(accessNotes)}</div>
</div>` : ''}
</div>`;
if (window.lucide) lucide.createIcons({ root: this });
if (lat && lng) {
this.#initMap(lat, lng, siteName, woNumber);
this.#setDirectionsLink(lat, lng);
}
this.#mounted = true;
}
#initMap(lat, lng, siteName, woNumber) {
if (typeof L === 'undefined') {
// Leaflet not yet loaded — retry once it fires
window.addEventListener('load', () => this.#initMap(lat, lng, siteName, woNumber), { once: true });
return;
}
const mapId = `leaflet-map-${this.getAttribute('wo-id') || 'map'}`;
const container = this.querySelector(`#${mapId}`);
if (!container) return;
// Prevent double-init
if (container._leaflet_id) return;
this.#map = L.map(container, { zoomControl: true }).setView([lat, lng], 16);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19,
}).addTo(this.#map);
const icon = L.divIcon({
className: '',
html: `<div style="
background: var(--teal, #0A7EA4);
color: #fff;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 6px rgba(0,0,0,.3);
font-size: 10px; font-weight: 700;">
<span style="transform:rotate(45deg)">${this.#esc(woNumber || '📍')}</span>
</div>`,
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32],
});
L.marker([lat, lng], { icon })
.addTo(this.#map)
.bindPopup(`<strong>${this.#esc(siteName)}</strong><br>${lat.toFixed(5)}, ${lng.toFixed(5)}`)
.openPopup();
}
#setDirectionsLink(lat, lng) {
const btn = this.querySelector('#directions-btn');
if (!btn) return;
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
btn.href = isIOS
? `maps://maps.apple.com/?daddr=${lat},${lng}&dirflg=d`
: `https://maps.google.com/maps?daddr=${lat},${lng}`;
}
#update() {
if (this.#map) {
this.#map.remove();
this.#map = null;
}
this.#mounted = false;
this.#build();
}
#esc(s) { return (s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
}
customElements.define('wo-map', WoMap);