feat(admin): enhance areas map interface with global view

Added mainMap to visualize all configured areas. Included reference layers for existing areas when editing or creating a new polygon to prevent overlap.
This commit is contained in:
2026-06-16 18:30:49 +02:00 Unverified
parent f32fa8620b
commit 220f613309
+116 -7
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, reactive, nextTick, onBeforeUnmount, computed } from 'vue'
import { ref, reactive, nextTick, onBeforeUnmount, computed, watch } from 'vue'
import { BARI_BOUNDS, BARI_CENTER, isWithinBariBounds } from '~/utils/bari'
definePageMeta({
@@ -24,8 +24,8 @@ const actionError = ref('')
const deletingAreaId = ref<number | null>(null)
const areaPendingDelete = ref<any | null>(null)
// Map variables
let map: any = null
let mainMap: any = null
let polygon: any = null
const mapPoints = ref<[number, number][]>([])
@@ -37,6 +37,95 @@ const areaDeleteMessage = computed(() =>
areaPendingDelete.value ? `Vuoi eliminare area "${areaPendingDelete.value.name}"? Operazione irreversibile.` : ''
)
const initMainMap = async () => {
const mapElement = document.getElementById('mainMap')
if (!mapElement) return
const L = (await import('leaflet')).default
if (mainMap) {
mainMap.remove()
mainMap = null
}
const bariBounds = L.latLngBounds(
[BARI_BOUNDS.south, BARI_BOUNDS.west],
[BARI_BOUNDS.north, BARI_BOUNDS.east]
)
mainMap = L.map('mainMap', {
maxBounds: bariBounds,
maxBoundsViscosity: 1,
minZoom: 11
}).setView(BARI_CENTER, 12)
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(mainMap)
const featureGroup = L.featureGroup().addTo(mainMap)
let hasLayers = false
areas.value.forEach((area: any) => {
if (area.boundary_geojson) {
try {
const layer = L.geoJSON(JSON.parse(area.boundary_geojson), {
style: {
color: '#0d6efd',
weight: 2,
fillOpacity: 0.2
}
}).bindPopup(`<strong>${area.name}</strong><br/>${area.description || ''}`)
layer.addTo(featureGroup)
hasLayers = true
} catch (e) {
console.error('Invalid geojson for area', area.id, e)
}
}
})
if (hasLayers) {
mainMap.fitBounds(featureGroup.getBounds())
} else {
mainMap.fitBounds(bariBounds)
}
}
watch([areas, pending], () => {
if (!pending.value) {
if (areas.value.length > 0) {
nextTick(() => {
initMainMap()
})
} else if (mainMap) {
mainMap.remove()
mainMap = null
}
}
}, { immediate: true })
const renderExistingAreasInModal = (L: any, currentMap: any) => {
areas.value.forEach((area: any) => {
if (area.id === editingAreaId.value) return
if (area.boundary_geojson) {
try {
L.geoJSON(JSON.parse(area.boundary_geojson), {
style: {
color: '#6c757d',
weight: 2,
dashArray: '5, 5',
fillOpacity: 0.1
},
interactive: false
}).addTo(currentMap)
} catch(e) {}
}
})
}
const openCreateModal = () => {
actionError.value = ''
editingAreaId.value = null
@@ -48,7 +137,6 @@ const openCreateModal = () => {
isCreating.value = true
nextTick(async () => {
// Import leaflet solo client-side
const L = (await import('leaflet')).default
if (map) {
@@ -74,6 +162,8 @@ const openCreateModal = () => {
polygon = L.polygon([], { color: 'red' }).addTo(map)
map.fitBounds(bariBounds)
renderExistingAreasInModal(L, map)
map.on('click', (e: any) => {
if (!isWithinBariBounds(e.latlng.lat, e.latlng.lng)) {
jsonError.value = 'Puoi disegnare aree solo entro confini del Comune di Bari.'
@@ -110,7 +200,7 @@ const openEditModal = (area: any) => {
editingAreaId.value = area.id
formData.name = area.name || ''
formData.description = area.description || ''
formData.geojson = area.boundary_geojson ? JSON.stringify(area.boundary_geojson, null, 2) : ''
formData.geojson = area.boundary_geojson || ''
jsonError.value = ''
mapPoints.value = []
isCreating.value = true
@@ -137,9 +227,20 @@ const openEditModal = (area: any) => {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(map)
console.log(JSON.parse(area.boundary_geojson))
L.geoJSON(JSON.parse(area.boundary_geojson), {
style: {
color: '#0d6efd',
weight: 2,
fillOpacity: 0.2
}
}).addTo(map)
polygon = L.polygon([], { color: 'red' }).addTo(map)
map.fitBounds(bariBounds)
renderExistingAreasInModal(L, map)
loadPolygonFromGeoJson(area.boundary_geojson)
map.on('click', (e: any) => {
@@ -171,7 +272,7 @@ const updateGeojson = () => {
}
const coords = mapPoints.value.map(p => [p[1], p[0]] as [number, number]) // Lng, Lat
if (coords[0]) {
coords.push([...coords[0]]) // Chiudi poligono
coords.push([...coords[0]])
}
formData.geojson = JSON.stringify({
@@ -191,6 +292,7 @@ const closeCreateModal = () => {
onBeforeUnmount(() => {
if (map) map.remove()
if (mainMap) mainMap.remove()
})
const handleCreate = async () => {
@@ -291,6 +393,12 @@ const handleDeleteArea = async () => {
Nessuna area configurata.
</div>
<template v-else>
<!-- Main view map showing all areas -->
<div class="card mb-4 shadow-sm border-0">
<div id="mainMap" class="rounded" style="height: 400px; width: 100%; z-index: 1;"></div>
</div>
<div v-if="actionError" class="alert alert-danger">
{{ actionError }}
</div>
@@ -332,8 +440,9 @@ const handleDeleteArea = async () => {
</tbody>
</table>
</div>
</template>
<!-- Modale Creazione -->
<!-- Modale Creazione/Modifica -->
<div v-if="isCreating" class="modal-backdrop fade show d-block" style="background: rgba(0,0,0,0.5); z-index: 1040;"></div>
<div v-if="isCreating" class="modal fade show d-block" tabindex="-1" style="z-index: 1050;">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
@@ -407,7 +516,7 @@ const handleDeleteArea = async () => {
</template>
<style scoped>
#areaMap {
#areaMap, #mainMap {
background: #e9ecef;
}