feat: implement Bari bounds validation and update UI components for admin panel

This commit is contained in:
gabdevele
2026-06-15 13:00:22 +02:00 Unverified
parent 8bf6f29561
commit d6eb7d3e9b
13 changed files with 108 additions and 34 deletions
+28 -5
View File
@@ -1,12 +1,13 @@
<script setup lang="ts">
import { ref, reactive, nextTick, onBeforeUnmount } from 'vue'
import { BARI_BOUNDS, BARI_CENTER, isWithinBariBounds } from '~/utils/bari'
definePageMeta({
middleware: 'auth',
auth: { role: 'ADMIN' }
})
useHead({ title: 'Gestione Aree - Admin' })
useHead({ title: 'Aree - Admin' })
const { getAreas, createArea } = useAdmin()
const { data: areasRes, pending, refresh } = useAsyncData('admin-areas', () => getAreas())
@@ -41,15 +42,31 @@ const openCreateModal = () => {
map = null
}
map = L.map('areaMap').setView([41.9028, 12.4964], 6)
const bariBounds = L.latLngBounds(
[BARI_BOUNDS.south, BARI_BOUNDS.west],
[BARI_BOUNDS.north, BARI_BOUNDS.east]
)
map = L.map('areaMap', {
maxBounds: bariBounds,
maxBoundsViscosity: 1,
minZoom: 12
}).setView(BARI_CENTER, 12)
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.fitBounds(bariBounds)
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.'
return
}
jsonError.value = ''
mapPoints.value.push([e.latlng.lat, e.latlng.lng])
polygon.setLatLngs(mapPoints.value)
updateGeojson()
@@ -102,6 +119,12 @@ const handleCreate = async () => {
if (!parsedGeojson.type || !parsedGeojson.coordinates) {
throw new Error('GeoJSON non valido. Richiesto "type" e "coordinates".')
}
const coordinates = parsedGeojson.coordinates?.[0] || []
const outsideBari = coordinates.some(([longitude, latitude]: [number, number]) => !isWithinBariBounds(latitude, longitude))
if (outsideBari) {
throw new Error('Area non valida. Tutti i punti devono essere nel Comune di Bari.')
}
} catch (e: any) {
jsonError.value = e.message || 'Formato JSON non valido.'
return
@@ -128,9 +151,9 @@ const handleCreate = async () => {
<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>
<h1 class="h3 font-weight-bold mb-0">Gestione Aree</h1>
<div>
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm me-2">Torna alla Dashboard</NuxtLink>
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm me-2">Torna al pannello admin</NuxtLink>
<button class="btn btn-primary btn-sm" @click="openCreateModal">
<BIIcon name="it-plus" size="xs" class="me-1" />
Nuova Area
@@ -218,7 +241,7 @@ const handleCreate = async () => {
</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.
Clicca sulla mappa per aggiungere vertici al poligono. Sono necessari almeno 3 punti. Sono ammessi solo punti nel Comune di Bari.
</div>
<div id="areaMap" class="rounded border" style="height: 400px; width: 100%; z-index: 1;"></div>
</div>
+1 -1
View File
@@ -86,7 +86,7 @@ const submitTeam = async () => {
<h1 class="h3 font-weight-bold mb-1">Categorie e Team</h1>
<p class="text-muted mb-0">Gestione dati di riferimento per segnalazioni e interventi.</p>
</div>
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm">Torna alla Dashboard</NuxtLink>
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm">Torna al pannello admin</NuxtLink>
</div>
<div class="row g-4">
+1 -1
View File
@@ -65,7 +65,7 @@ const formatDate = (dateStr: string) => {
<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">Moderazione Abusi</h1>
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm">Torna alla Dashboard</NuxtLink>
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm">Torna al pannello admin</NuxtLink>
</div>
<div class="row mb-4">
+2 -1
View File
@@ -76,7 +76,7 @@ const saveRoles = async () => {
<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 Utenti</h1>
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm">Torna alla Dashboard</NuxtLink>
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm">Torna al pannello admin</NuxtLink>
</div>
<div class="row mb-4">
@@ -162,6 +162,7 @@ const saveRoles = async () => {
type="checkbox"
:value="role"
:id="'role-'+role"
:checked="selectedRoles.includes(role)"
v-model="selectedRoles"
>
<label class="form-check-label" :for="'role-'+role">
+2 -2
View File
@@ -19,14 +19,14 @@ const currentDashboard = computed(() => {
const heroTitle = computed(() => {
const role = user.value?.role?.toUpperCase()
if (role === 'ADMIN') return 'Pannello di Amministrazione'
if (role === 'ADMIN') return 'Pannello Admin'
if (role === 'OPERATOR') return 'Gestione Operativa'
return 'Dashboard'
})
const heroSubtitle = computed(() => {
const role = user.value?.role?.toUpperCase()
if (role === 'ADMIN') return 'Gestione del sistema, delle aree e moderazione utenti'
if (role === 'ADMIN') return 'Accesso amministrativo per configurazioni, utenti e moderazione'
if (role === 'OPERATOR') return 'Prendi in carico e gestisci le segnalazioni in corso'
return 'Riepilogo rapido delle tue segnalazioni e dei punti cittadino'
})
+2 -2
View File
@@ -87,7 +87,7 @@ useHead({
<BIIcon name="it-settings" size="xl" color="primary" class="mb-3" />
<h3 class="card-title h5 font-weight-bold">Amministrazione</h3>
<p class="card-text text-muted">
Gli amministratori hanno il controllo completo della piattaforma. Possono definire le aree urbane (tramite poligoni GeoJSON), gestire i permessi degli utenti, supervisionare i feedback e moderare eventuali abusi.
Gli amministratori hanno il controllo completo della piattaforma. Possono definire le aree urbane tramite poligoni GeoJSON, gestire i permessi degli utenti, supervisionare i feedback e moderare eventuali abusi.
<br><br>
Hanno inoltre accesso a statistiche avanzate per individuare le criticità sul territorio e monitorare i tempi medi di risoluzione.
</p>
@@ -175,4 +175,4 @@ useHead({
list-style-type: none;
padding-left: 0;
}
</style>
</style>
+7 -2
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { isWithinBariBounds } from '~/utils/bari'
// 1. API Fetching Logic
const { getOpenDataReports } = usePublic()
@@ -15,6 +16,10 @@ const { data: openDataResponse, pending } = await useAsyncData(
{ watch: [currentPage] } // Nuxt will re-fetch when currentPage changes
)
const visibleReports = computed(() =>
(openDataResponse.value?.reports || []).filter((report) => isWithinBariBounds(report.latitude, report.longitude))
)
// Pagination logic: If we receive less items than `perPage`, we are on the last page.
const isLastPage = computed(() => {
if (!openDataResponse.value?.reports) return true
@@ -102,7 +107,7 @@ const getStatusBadge = (status: string) => {
<!-- GRID VIEW -->
<div v-else-if="currentView === 'grid'">
<div class="row" v-if="openDataResponse?.reports && openDataResponse.reports.length > 0">
<div class="col-12 col-md-6 col-lg-4 mb-4" v-for="report in openDataResponse.reports" :key="report.id">
<div class="col-12 col-md-6 col-lg-4 mb-4" v-for="report in visibleReports" :key="report.id">
<!-- Strict BI Card -->
<div class="card-wrapper card-space h-100">
<div class="card card-bg h-100 shadow-sm">
@@ -179,7 +184,7 @@ const getStatusBadge = (status: string) => {
<div class="text-center text-muted">
<BIIcon name="it-map" size="xl" class="mb-3 opacity-50" />
<h4 class="h5">Mappa Interattiva</h4>
<p>Caricamento dei {{ openDataResponse?.reports?.length || 0 }} punti geografici in corso...</p>
<p>Caricamento dei {{ visibleReports.length || 0 }} punti geografici in corso...</p>
</div>
</div>
</div>
+4 -1
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { Report } from '~/types/api'
import { isWithinBariBounds } from '~/utils/bari'
definePageMeta({
middleware: 'auth',
@@ -15,7 +16,9 @@ const filterStatus = ref('')
const { data: reportsRes, pending } = useAsyncData('citizen-reports-list', () => getReports({}))
const reports = computed<Report[]>(() => reportsRes.value?.reports || [])
const reports = computed<Report[]>(() =>
(reportsRes.value?.reports || []).filter((report) => isWithinBariBounds(report.latitude, report.longitude))
)
const filteredReports = computed(() => {
if (!filterStatus.value) return reports.value
if (filterStatus.value === 'IN_PROGRESS') {