feat(admin): implement admin dashboard pages for users, moderation and areas
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, nextTick, onBeforeUnmount } from 'vue'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
auth: { role: 'ADMIN' }
|
||||
})
|
||||
|
||||
useHead({ title: 'Gestione Aree - Admin' })
|
||||
|
||||
const { getAreas, createArea } = useAdmin()
|
||||
const { data: areasRes, pending, refresh } = useAsyncData('admin-areas', () => getAreas())
|
||||
|
||||
const isCreating = ref(false)
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
geojson: ''
|
||||
})
|
||||
const jsonError = ref('')
|
||||
|
||||
// Map variables
|
||||
let map: any = null
|
||||
let polygon: any = null
|
||||
const mapPoints = ref<[number, number][]>([])
|
||||
|
||||
const openCreateModal = () => {
|
||||
formData.name = ''
|
||||
formData.description = ''
|
||||
formData.geojson = ''
|
||||
jsonError.value = ''
|
||||
mapPoints.value = []
|
||||
isCreating.value = true
|
||||
|
||||
nextTick(async () => {
|
||||
// Import leaflet solo client-side
|
||||
const L = (await import('leaflet')).default
|
||||
|
||||
if (map) {
|
||||
map.remove()
|
||||
map = null
|
||||
}
|
||||
|
||||
map = L.map('areaMap').setView([41.9028, 12.4964], 6)
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap'
|
||||
}).addTo(map)
|
||||
|
||||
polygon = L.polygon([], { color: 'red' }).addTo(map)
|
||||
|
||||
map.on('click', (e: any) => {
|
||||
mapPoints.value.push([e.latlng.lat, e.latlng.lng])
|
||||
polygon.setLatLngs(mapPoints.value)
|
||||
updateGeojson()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const clearMap = () => {
|
||||
mapPoints.value = []
|
||||
if (polygon) {
|
||||
polygon.setLatLngs([])
|
||||
}
|
||||
formData.geojson = ''
|
||||
}
|
||||
|
||||
const updateGeojson = () => {
|
||||
if (mapPoints.value.length < 3) {
|
||||
formData.geojson = ''
|
||||
return
|
||||
}
|
||||
const coords = mapPoints.value.map(p => [p[1], p[0]] as [number, number]) // Lng, Lat
|
||||
if (coords[0]) {
|
||||
coords.push([...coords[0]]) // Chiudi poligono
|
||||
}
|
||||
|
||||
formData.geojson = JSON.stringify({
|
||||
type: "Polygon",
|
||||
coordinates: [coords]
|
||||
}, null, 2)
|
||||
}
|
||||
|
||||
const closeCreateModal = () => {
|
||||
isCreating.value = false
|
||||
if (map) {
|
||||
map.remove()
|
||||
map = null
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (map) map.remove()
|
||||
})
|
||||
|
||||
const handleCreate = async () => {
|
||||
jsonError.value = ''
|
||||
let parsedGeojson = null
|
||||
|
||||
try {
|
||||
parsedGeojson = JSON.parse(formData.geojson)
|
||||
if (!parsedGeojson.type || !parsedGeojson.coordinates) {
|
||||
throw new Error('GeoJSON non valido. Richiesto "type" e "coordinates".')
|
||||
}
|
||||
} catch (e: any) {
|
||||
jsonError.value = e.message || 'Formato JSON non valido.'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await createArea({
|
||||
name: formData.name,
|
||||
description: formData.description || undefined,
|
||||
boundary_geojson: parsedGeojson
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
await refresh()
|
||||
closeCreateModal()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to create area', err)
|
||||
alert('Errore durante la creazione dell\'area.')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container my-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 font-weight-bold mb-0">Gestione Aree GeoJSON</h1>
|
||||
<div>
|
||||
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm me-2">Dashboard</NuxtLink>
|
||||
<button class="btn btn-primary btn-sm" @click="openCreateModal">
|
||||
<BIIcon name="it-plus" size="xs" class="me-1" />
|
||||
Nuova Area
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="pending" class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!areasRes?.areas || areasRes.areas.length === 0" class="alert alert-info">
|
||||
Nessuna area configurata.
|
||||
</div>
|
||||
|
||||
<div v-else class="table-responsive">
|
||||
<table class="table table-hover align-middle bg-white shadow-sm rounded">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nome</th>
|
||||
<th>Descrizione</th>
|
||||
<th>Stato</th>
|
||||
<th>GeoJSON</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="area in areasRes.areas" :key="area.id">
|
||||
<td>{{ area.id }}</td>
|
||||
<td><strong>{{ area.name }}</strong></td>
|
||||
<td>{{ area.description || '-' }}</td>
|
||||
<td>
|
||||
<span v-if="area.active" class="badge bg-success">Attiva</span>
|
||||
<span v-else class="badge bg-secondary">Inattiva</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="area.boundary_geojson" class="badge border border-primary text-primary">Configurato</span>
|
||||
<span v-else class="badge border border-danger text-danger">Mancante</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Modale Creazione -->
|
||||
<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">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Nuova Area</h5>
|
||||
<button type="button" class="btn-close" @click="closeCreateModal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form @submit.prevent="handleCreate">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nome Area</label>
|
||||
<input type="text" class="form-control" v-model="formData.name" required placeholder="Es. Centro Storico" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Descrizione (opzionale)</label>
|
||||
<textarea class="form-control" v-model="formData.description" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">GeoJSON Generato</label>
|
||||
<textarea
|
||||
class="form-control font-monospace text-sm"
|
||||
rows="6"
|
||||
v-model="formData.geojson"
|
||||
required
|
||||
readonly
|
||||
placeholder='{ "type": "Polygon", "coordinates": [...] }'
|
||||
></textarea>
|
||||
<div v-if="jsonError" class="form-text text-danger mt-1">{{ jsonError }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-7">
|
||||
<div class="mb-2 d-flex justify-content-between align-items-center">
|
||||
<label class="form-label mb-0">Disegna Poligono sulla Mappa</label>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" @click="clearMap">
|
||||
Pulisci Mappa
|
||||
</button>
|
||||
</div>
|
||||
<div class="alert alert-light border small mb-2 p-2">
|
||||
Clicca sulla mappa per aggiungere vertici al poligono. Sono necessari almeno 3 punti.
|
||||
</div>
|
||||
<div id="areaMap" class="rounded border" style="height: 400px; width: 100%; z-index: 1;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer d-flex justify-content-end gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary" @click="closeCreateModal">Annulla</button>
|
||||
<button type="button" class="btn btn-primary" :disabled="!formData.name || !formData.geojson" @click="handleCreate">
|
||||
Crea Area
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#areaMap {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin-block: 0!important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user