diff --git a/src/pages/dashboards/home/blocks/SitesateliteMap.tsx b/src/pages/dashboards/home/blocks/SitesateliteMap.tsx index 51b0bc2..584dd70 100644 --- a/src/pages/dashboards/home/blocks/SitesateliteMap.tsx +++ b/src/pages/dashboards/home/blocks/SitesateliteMap.tsx @@ -64,6 +64,145 @@ const pinIcon = (multi: boolean) => { }); }; +// --------------------------------------------------------------------------- +// "Signal detail" popup (blue pins only) — mimics the tabbed +// Signal / RAN 2G / RAN 3G / Alarm dialog from the reference screenshot. +// --------------------------------------------------------------------------- + +const QUALITY_LEVELS = ['Low', 'Moderate', 'Good', 'Very Good', 'Excellent']; + +// Deterministic PLACEHOLDER quality derived from the site name — replace +// with a real lookup (RSRP/RSSI, KPI feed, etc.) once that data exists. +const placeholderQuality = (seed: string, offset: number) => { + let hash = 0; + for (let i = 0; i < seed.length; i++) hash = (hash * 31 + seed.charCodeAt(i)) >>> 0; + const level = (hash + offset) % QUALITY_LEVELS.length; + return { level: level + 1, label: QUALITY_LEVELS[level] }; +}; + +const signalBarsHtml = (level: number) => { + const heights = [5, 9, 13, 17]; + return `
+ ${heights + .map( + (h, i) => + `
` + ) + .join('')} +
`; +}; + +const signalCardHtml = (title: string, desc: string, seed: string, offset: number) => { + const q = placeholderQuality(seed, offset); + return ` +
+
${title}
+
${signalBarsHtml(q.level)}
+
${q.label}
+
${desc}
+
`; +}; + +const cellListHtml = (points: SitePoint[]) => { + if (points.length === 0) { + return `
No cells in this band.
`; + } + const rows = points + .map((p) => `${p.cell}${p.sector}`) + .join(''); + return `${rows}
`; +}; + +const buildSignalPopupHtml = (points: SitePoint[], lat: number, lng: number) => { + const site = points[0].site; + const location = points[0].location; + + // Placeholder split: cells ending in "E" shown under RAN 2G, cells ending + // in "F" under RAN 3G. The source data doesn't label technology + // explicitly — adjust this rule once the real mapping is known. + const ran2g = points.filter((p) => p.cell.trim().toUpperCase().endsWith('E')); + const ran3g = points.filter((p) => p.cell.trim().toUpperCase().endsWith('F')); + + return ` +
+
+
${site}
+
${location} · ${lat.toFixed(5)}, ${lng.toFixed(5)}
+
+ +
+ + + + +
+ +
+
+ ${signalCardHtml('3G', 'Browsing, email, video call', site, 0)} + ${signalCardHtml('Mobile BB', 'Laptop browsing & downloads', site, 1)} + ${signalCardHtml('2G', 'Calling and texting', site, 2)} +
+
+ + +
+
+ + + + + + +
`; +}; + +const tabBtnStyle = (active: boolean) => + `flex:1;padding:5px 0;font-size:11px;font-weight:600;border-radius:6px;border:none;cursor:pointer;` + + (active ? 'background:#2563eb;color:#ffffff;' : 'background:#f1f5f9;color:#475569;'); + +const actionBtnStyle = (bg: string, color: string) => + `flex:1;padding:6px 0;font-size:12px;font-weight:600;border-radius:6px;cursor:pointer;` + + `background:${bg};color:${color};border:1px solid #2563eb;`; + +// Wires up tab-switching and Suggest/Complaint clicks for a just-opened +// signal popup. Leaflet popups are plain HTML, so this is done imperatively +// rather than through React state. +const wireSignalPopup = (container: HTMLElement, site: string) => { + const tabs = container.querySelectorAll('.signal-tab'); + const contents = container.querySelectorAll('.signal-content'); + + tabs.forEach((btn) => { + btn.addEventListener('click', () => { + const target = btn.dataset.tab; + tabs.forEach((b) => { + const isActive = b === btn; + b.setAttribute('style', tabBtnStyle(isActive)); + }); + contents.forEach((c) => { + c.style.display = c.dataset.content === target ? 'block' : 'none'; + }); + }); + }); + + container.querySelectorAll('[data-action]').forEach((btn) => { + btn.addEventListener('click', () => { + // TODO: wire these up to real endpoints once available. + console.log(`[SitesSatelliteMap] "${btn.dataset.action}" clicked for site "${site}"`); + }); + }); +}; + const SitesSatelliteMap = () => { const mapContainerRef = useRef(null); const mapRef = useRef(null); @@ -105,21 +244,27 @@ const SitesSatelliteMap = () => { grouped.forEach((points, key) => { const [lat, lng] = key.split(',').map(Number); - const marker = L.marker([lat, lng], { icon: pinIcon(points.length > 1) }); + const isMulti = points.length > 1; + const marker = L.marker([lat, lng], { icon: pinIcon(isMulti) }); - const site = points[0].site; - const location = points[0].location; - const rows = points - .map((p) => `${p.cell}${p.sector}`) - .join(''); - - marker.bindPopup(` -
-
${site}
-
${location} · ${lat.toFixed(5)}, ${lng.toFixed(5)}
- ${rows}
-
- `); + if (isMulti) { + // Blue pin -> rich "Signal" dialog. + marker.bindPopup(buildSignalPopupHtml(points, lat, lng), { maxWidth: 320, minWidth: 290 }); + } else { + // Red pin -> simple cell/sector info (unchanged). + const site = points[0].site; + const location = points[0].location; + const rows = points + .map((p) => `${p.cell}${p.sector}`) + .join(''); + marker.bindPopup(` +
+
${site}
+
${location} · ${lat.toFixed(5)}, ${lng.toFixed(5)}
+ ${rows}
+
+ `); + } markerByKeyRef.current.set(key, marker); cluster.addLayer(marker); @@ -127,7 +272,19 @@ const SitesSatelliteMap = () => { map.addLayer(cluster); + // Wire up the rich popup's interactive bits (tabs + buttons) every time + // one is opened, since Leaflet re-renders the DOM content on each open. + const onPopupOpen = (e: L.PopupEvent) => { + const el = e.popup.getElement(); + const content = el?.querySelector('.signal-popup'); + if (!content) return; + const siteName = content.querySelector('div')?.textContent ?? ''; + wireSignalPopup(content, siteName); + }; + map.on('popupopen', onPopupOpen); + return () => { + map.off('popupopen', onPopupOpen); map.remove(); mapRef.current = null; clusterRef.current = null;