diff --git a/app/indaba/(ops)/map/page.tsx b/app/indaba/(ops)/map/page.tsx index 0f509ba..123d37c 100644 --- a/app/indaba/(ops)/map/page.tsx +++ b/app/indaba/(ops)/map/page.tsx @@ -34,7 +34,7 @@ type LinkRow = Pick< | "payment_frequency" >; -type ZoneRow = Pick; +type ZoneRow = Pick; type IntroductionRow = Pick< Introduction, @@ -71,7 +71,7 @@ export default async function MapPage() { .select( "id, supplier_id, buyer_id, product, est_monthly_volume, payment_frequency", ), - supabase.from("zones").select("id, name, centre_lat, centre_lng"), + supabase.from("zones").select("id, name, type, centre_lat, centre_lng, boundary_geojson"), canSeeIntros ? supabase .from("introductions") @@ -172,8 +172,10 @@ export default async function MapPage() { const zones: MapZone[] = zoneRows.map((z) => ({ id: z.id, name: z.name, + type: z.type ?? null, centre_lat: z.centre_lat != null ? Number(z.centre_lat) : null, centre_lng: z.centre_lng != null ? Number(z.centre_lng) : null, + boundary_geojson: z.boundary_geojson ?? null, })); diff --git a/components/ops/Map/BulawayoMap.tsx b/components/ops/Map/BulawayoMap.tsx index e002cd4..115bfac 100644 --- a/components/ops/Map/BulawayoMap.tsx +++ b/components/ops/Map/BulawayoMap.tsx @@ -56,6 +56,23 @@ function hashJitter(id: string, salt: number): number { return (n / 1000) * 0.004 - 0.002; } + +const ZONE_TYPE_COLORS: Record = { + suburb: "#3B82F6", + industrial: "#F97316", + market: "#DC2626", + cbd: "#FACC15", + residential: "#22C55E", + ground_ops_priority_1: "#D4AF37", + ground_ops_secondary: "#14B8A6", + wholesale: "#A855F7", +}; + +function zoneColor(type: string | null): string { + if (!type) return "#64748B"; + return ZONE_TYPE_COLORS[type] ?? "#64748B"; +} + export default function BulawayoMap({ businesses, discoveryCandidates, @@ -86,6 +103,8 @@ export default function BulawayoMap({ const onMapClickRef = useRef(onMapClick); const layersRef = useRef<{ links: L.LayerGroup; + zones: L.LayerGroup; + zoneLabels: L.LayerGroup; businesses: L.LayerGroup; introductions: L.LayerGroup; } | null>(null); @@ -120,13 +139,25 @@ export default function BulawayoMap({ }, ).addTo(map); + map.createPane("zonesPane"); + const zonesPane = map.getPane("zonesPane"); + if (zonesPane) zonesPane.style.zIndex = "350"; + + map.createPane("zoneLabelsPane"); + const zoneLabelsPane = map.getPane("zoneLabelsPane"); + if (zoneLabelsPane) zoneLabelsPane.style.zIndex = "450"; + + const zonesLayer = L.layerGroup().addTo(map); const linksLayer = L.layerGroup().addTo(map); + const zoneLabelsLayer = L.layerGroup().addTo(map); const businessesLayer = L.layerGroup().addTo(map); const introductionsLayer = L.layerGroup().addTo(map); mapRef.current = map; layersRef.current = { + zones: zonesLayer, links: linksLayer, + zoneLabels: zoneLabelsLayer, businesses: businessesLayer, introductions: introductionsLayer, }; @@ -142,6 +173,8 @@ export default function BulawayoMap({ const layers = layersRef.current; if (!layers) return; + layers.zones.clearLayers(); + layers.zoneLabels.clearLayers(); layers.businesses.clearLayers(); layers.links.clearLayers(); layers.introductions.clearLayers(); @@ -151,6 +184,50 @@ export default function BulawayoMap({ const showIntroLayer = showIntros; const showLinkLayer = true; + for (const zone of zones) { + if (zone.boundary_geojson) { + const color = zoneColor(zone.type ?? null); + const layer = L.geoJSON(zone.boundary_geojson as unknown as GeoJSON.GeoJsonObject, { + pane: "zonesPane", + style: { + color, + weight: 1, + opacity: 0.4, + fillColor: color, + fillOpacity: 0.15, + }, + }); + + layer.eachLayer((featureLayer) => { + if (!(featureLayer instanceof L.Path)) return; + featureLayer.bindTooltip(zone.name, { sticky: true }); + featureLayer.on("mouseover", () => { + featureLayer.setStyle({ fillOpacity: 0.3 }); + }); + featureLayer.on("mouseout", () => { + featureLayer.setStyle({ fillOpacity: 0.15 }); + }); + }); + + layer.addTo(layers.zones); + } + + if (zone.centre_lat != null && zone.centre_lng != null) { + const labelIcon = L.divIcon({ + className: "indaba-zone-label", + html: `
${escapeHtml(zone.name)}
`, + iconSize: [0, 0], + }); + + L.marker([zone.centre_lat, zone.centre_lng], { + icon: labelIcon, + pane: "zoneLabelsPane", + interactive: false, + keyboard: false, + }).addTo(layers.zoneLabels); + } + } + if (showBusinessLayer) { for (const b of businesses) { if (b.lat == null || b.lng == null) continue; @@ -311,7 +388,7 @@ export default function BulawayoMap({ .addTo(layers.introductions); } } - }, [businesses, discoveryCandidates, links, introductions, showBrendon, showTafadzwa, showUnattributed, showIntros, showCandidates, zoneNameById, onEditBusiness, onMoveBusiness, onDeleteBusiness, onLogInteraction, onOpenCandidate, onSelectBusiness]); + }, [businesses, discoveryCandidates, links, zones, introductions, showBrendon, showTafadzwa, showUnattributed, showIntros, showCandidates, zoneNameById, onEditBusiness, onMoveBusiness, onDeleteBusiness, onLogInteraction, onOpenCandidate, onSelectBusiness]); useEffect(() => { const map = mapRef.current; diff --git a/components/ops/Map/types.ts b/components/ops/Map/types.ts index b5e1430..5d68207 100644 --- a/components/ops/Map/types.ts +++ b/components/ops/Map/types.ts @@ -42,8 +42,10 @@ export type MapLink = { export type MapZone = { id: string; name: string; + type?: string | null; centre_lat: number | null; centre_lng: number | null; + boundary_geojson?: Record | null; }; export type MapIntroduction = {