'use client' import { useEffect, useRef, useState } from 'react' import { useRouter } from 'next/navigation' import axios from 'axios' const API = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1' interface Zone { id: string name: string slug: string status: 'active' | 'coming_soon' | 'inactive' priority: number radius_km: number center_lng: number center_lat: number boundary_geojson: string restaurant_count: number driver_count: number } const STATUS_CONFIG = { active: { label: 'Active', color: 'bg-green-100 text-green-800 border-green-200', dot: 'bg-green-500' }, coming_soon: { label: 'Coming Soon', color: 'bg-amber-100 text-amber-800 border-amber-200', dot: 'bg-amber-400' }, inactive: { label: 'Inactive', color: 'bg-gray-100 text-gray-600 border-gray-200', dot: 'bg-gray-400' }, } function ZoneMap({ zones, selectedId, onSelect }: { zones: Zone[]; selectedId: string | null; onSelect: (id: string) => void }) { const mapRef = useRef(null) const mapInstance = useRef(null) useEffect(() => { if (!mapRef.current || typeof window === 'undefined') return import('maplibre-gl').then(({ default: maplibre }) => { if (mapInstance.current) return const map = new maplibre.Map({ container: mapRef.current!, style: `https://api.maptiler.com/maps/streets/style.json?key=${process.env.NEXT_PUBLIC_MAPTILER_KEY || 'demo'}`, center: [-79.38, 43.65], zoom: 9, }) mapInstance.current = map map.on('load', () => { // Add zone boundaries as a GeoJSON layer const features = zones .filter(z => z.boundary_geojson) .map(z => ({ type: 'Feature' as const, properties: { id: z.id, name: z.name, status: z.status }, geometry: JSON.parse(z.boundary_geojson), })) map.addSource('zones', { type: 'geojson', data: { type: 'FeatureCollection', features }, }) // Fill map.addLayer({ id: 'zones-fill', type: 'fill', source: 'zones', paint: { 'fill-color': [ 'match', ['get', 'status'], 'active', '#0d9488', 'coming_soon', '#f59e0b', '#9ca3af', ], 'fill-opacity': 0.15, }, }) // Outline map.addLayer({ id: 'zones-outline', type: 'line', source: 'zones', paint: { 'line-color': [ 'match', ['get', 'status'], 'active', '#0d9488', 'coming_soon', '#f59e0b', '#9ca3af', ], 'line-width': 2, }, }) // Labels map.addLayer({ id: 'zones-label', type: 'symbol', source: 'zones', layout: { 'text-field': ['get', 'name'], 'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'], 'text-size': 12, }, paint: { 'text-color': '#1f2937', 'text-halo-color': '#ffffff', 'text-halo-width': 2, }, }) // Click handler map.on('click', 'zones-fill', (e) => { const id = e.features?.[0]?.properties?.id if (id) onSelect(id) }) map.on('mouseenter', 'zones-fill', () => { map.getCanvas().style.cursor = 'pointer' }) map.on('mouseleave', 'zones-fill', () => { map.getCanvas().style.cursor = '' }) }) }) return () => { mapInstance.current?.remove() mapInstance.current = null } }, []) // only mount once // Highlight selected zone useEffect(() => { if (!mapInstance.current || !selectedId) return const map = mapInstance.current if (!map.getLayer('zones-fill')) return map.setPaintProperty('zones-fill', 'fill-opacity', [ 'case', ['==', ['get', 'id'], selectedId], 0.35, 0.15, ]) }, [selectedId]) return (
) } export default function AdminZonesPage() { const router = useRouter() const [zones, setZones] = useState([]) const [selected, setSelected] = useState(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [message, setMessage] = useState('') useEffect(() => { const token = localStorage.getItem('token') if (!token) { router.push('/login'); return } fetchZones(token) }, []) const fetchZones = async (token?: string) => { const t = token || localStorage.getItem('token') try { const { data } = await axios.get(`${API}/admin/zones`, { headers: { Authorization: `Bearer ${t}` }, }) setZones(data) if (data.length > 0 && !selected) setSelected(data[0]) } catch { router.push('/login') } finally { setLoading(false) } } const handleStatusChange = async (zoneId: string, status: Zone['status']) => { setSaving(true) setMessage('') try { const token = localStorage.getItem('token') await axios.patch( `${API}/admin/zones/${zoneId}/status`, { status }, { headers: { Authorization: `Bearer ${token}` } }, ) setZones(prev => prev.map(z => z.id === zoneId ? { ...z, status } : z)) setSelected(prev => prev?.id === zoneId ? { ...prev, status } : prev) setMessage(`Zone status updated to "${STATUS_CONFIG[status].label}"`) setTimeout(() => setMessage(''), 3000) } catch (err: any) { setMessage('Failed to update zone status') } finally { setSaving(false) } } const activeZones = zones.filter(z => z.status === 'active') const comingSoonZones = zones.filter(z => z.status === 'coming_soon') return (
{/* Header */}

Zone Management

{activeZones.length} active {comingSoonZones.length} coming soon
{message && (
{message}
)}
{/* Zone list */}

GTA Delivery Zones

{loading ? (
Loading zones...
) : ( zones.map(zone => { const cfg = STATUS_CONFIG[zone.status] const isSelected = selected?.id === zone.id return ( ) }) )}
{/* Zone detail + map */}
{/* Map */}
{!loading && setSelected(zones.find(z => z.id === id) || null)} />}
{/* Zone editor */} {selected && (

{selected.name}

{selected.center_lat?.toFixed(4)}, {selected.center_lng?.toFixed(4)} · {selected.radius_km}km radius

{selected.restaurant_count}

restaurants

{selected.driver_count}

drivers

{selected.priority}

priority

{/* Status controls */}

Zone Status

{(['active', 'coming_soon', 'inactive'] as const).map(s => { const cfg = STATUS_CONFIG[s] const isCurrent = selected.status === s return ( ) })}
{/* Impact warning */} {selected.status === 'active' && (
Live zone: {selected.restaurant_count} restaurants and {selected.driver_count} drivers are actively using this zone. Deactivating will prevent new orders in this area.
)} {selected.status === 'coming_soon' && (
Activating this zone will open delivery for restaurants and drivers in {selected.name}. Make sure you have drivers ready!
)}
)} {/* Launch checklist */}

Zone Launch Checklist

{zones.map(z => (
{z.name} {z.restaurant_count}r · {z.driver_count}d {STATUS_CONFIG[z.status].label}
))}
) }