Compare commits
3
Commits
5f8e83ba8c
...
7fafa843c1
@@ -12,28 +12,59 @@ const { data: statsRes, pending } = useAsyncData('admin-stats', () => getOvervie
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-5" v-if="!pending && statsRes?.ok">
|
<div class="row mb-5" v-if="!pending && statsRes?.ok">
|
||||||
|
|
||||||
<div class="col-6 col-md-3 mb-3">
|
<div class="col-6 col-md-3 mb-3">
|
||||||
<div class="card-wrapper card-space">
|
<div class="card-wrapper card-space">
|
||||||
<div class="card card-bg text-center">
|
<div class="card card-bg text-center">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="text-primary">{{ statsRes.total_users || 0 }}</h2>
|
<h2 class="text-primary">
|
||||||
<p class="card-text">Utenti Registrati</p>
|
{{ statsRes.reports_by_status?.reduce((sum, item) => sum + Number(item.total), 0) || 0 }}
|
||||||
|
</h2>
|
||||||
|
<p class="card-text">Totale Segnalazioni</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3 mb-3">
|
||||||
|
<div class="card-wrapper card-space">
|
||||||
|
<div class="card card-bg text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="text-danger">
|
||||||
|
{{ statsRes.reports_by_status?.find(s => s.status === 'OPEN')?.total ?? 0 }}
|
||||||
|
</h2>
|
||||||
|
<p class="card-text">Aperte (Da Gestire)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-6 col-md-3 mb-3">
|
<div class="col-6 col-md-3 mb-3">
|
||||||
<div class="card-wrapper card-space">
|
<div class="card-wrapper card-space">
|
||||||
<div class="card card-bg text-center">
|
<div class="card card-bg text-center">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="text-warning">
|
<h2 class="text-warning">
|
||||||
{{ statsRes.open_reports ?? statsRes.reports_by_status?.find(s => s.status === 'OPEN')?.total ?? 0 }}
|
{{ statsRes.reports_by_status?.find(s => s.status === 'ASSIGNED')?.total ?? 0 }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="card-text">Segn. Aperte</p>
|
<p class="card-text">In Carico (Assegnate)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3 mb-3">
|
||||||
|
<div class="card-wrapper card-space">
|
||||||
|
<div class="card card-bg text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="text-success">
|
||||||
|
{{ statsRes.reports_by_status?.find(s => s.status === 'CLOSED')?.total ?? 0 }}
|
||||||
|
</h2>
|
||||||
|
<p class="card-text">Risolte (Chiuse)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="mb-3">Gestione Sistema</h3>
|
<h3 class="mb-3">Gestione Sistema</h3>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const formData = reactive({
|
|||||||
})
|
})
|
||||||
const jsonError = ref('')
|
const jsonError = ref('')
|
||||||
const actionError = ref('')
|
const actionError = ref('')
|
||||||
|
const modalError = ref('')
|
||||||
const deletingAreaId = ref<number | null>(null)
|
const deletingAreaId = ref<number | null>(null)
|
||||||
const areaPendingDelete = ref<any | null>(null)
|
const areaPendingDelete = ref<any | null>(null)
|
||||||
|
|
||||||
@@ -121,13 +122,15 @@ const renderExistingAreasInModal = (L: any, currentMap: any) => {
|
|||||||
},
|
},
|
||||||
interactive: false
|
interactive: false
|
||||||
}).addTo(currentMap)
|
}).addTo(currentMap)
|
||||||
} catch(e) {}
|
} catch (e) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const openCreateModal = () => {
|
const openCreateModal = () => {
|
||||||
actionError.value = ''
|
actionError.value = ''
|
||||||
|
modalError.value = ''
|
||||||
editingAreaId.value = null
|
editingAreaId.value = null
|
||||||
formData.name = ''
|
formData.name = ''
|
||||||
formData.description = ''
|
formData.description = ''
|
||||||
@@ -171,6 +174,7 @@ const openCreateModal = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jsonError.value = ''
|
jsonError.value = ''
|
||||||
|
modalError.value = ''
|
||||||
mapPoints.value.push([e.latlng.lat, e.latlng.lng])
|
mapPoints.value.push([e.latlng.lat, e.latlng.lng])
|
||||||
polygon.setLatLngs(mapPoints.value)
|
polygon.setLatLngs(mapPoints.value)
|
||||||
updateGeojson()
|
updateGeojson()
|
||||||
@@ -197,6 +201,7 @@ const loadPolygonFromGeoJson = (boundaryGeojson: any) => {
|
|||||||
|
|
||||||
const openEditModal = (area: any) => {
|
const openEditModal = (area: any) => {
|
||||||
actionError.value = ''
|
actionError.value = ''
|
||||||
|
modalError.value = ''
|
||||||
editingAreaId.value = area.id
|
editingAreaId.value = area.id
|
||||||
formData.name = area.name || ''
|
formData.name = area.name || ''
|
||||||
formData.description = area.description || ''
|
formData.description = area.description || ''
|
||||||
@@ -250,6 +255,7 @@ const openEditModal = (area: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jsonError.value = ''
|
jsonError.value = ''
|
||||||
|
modalError.value = ''
|
||||||
mapPoints.value.push([e.latlng.lat, e.latlng.lng])
|
mapPoints.value.push([e.latlng.lat, e.latlng.lng])
|
||||||
polygon.setLatLngs(mapPoints.value)
|
polygon.setLatLngs(mapPoints.value)
|
||||||
updateGeojson()
|
updateGeojson()
|
||||||
@@ -263,6 +269,7 @@ const clearMap = () => {
|
|||||||
polygon.setLatLngs([])
|
polygon.setLatLngs([])
|
||||||
}
|
}
|
||||||
formData.geojson = ''
|
formData.geojson = ''
|
||||||
|
modalError.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateGeojson = () => {
|
const updateGeojson = () => {
|
||||||
@@ -270,7 +277,7 @@ const updateGeojson = () => {
|
|||||||
formData.geojson = ''
|
formData.geojson = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const coords = mapPoints.value.map(p => [p[1], p[0]] as [number, number]) // Lng, Lat
|
const coords = mapPoints.value.map(p => [p[1], p[0]] as [number, number])
|
||||||
if (coords[0]) {
|
if (coords[0]) {
|
||||||
coords.push([...coords[0]])
|
coords.push([...coords[0]])
|
||||||
}
|
}
|
||||||
@@ -297,6 +304,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
jsonError.value = ''
|
jsonError.value = ''
|
||||||
|
modalError.value = ''
|
||||||
let parsedGeojson = null
|
let parsedGeojson = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -331,9 +339,16 @@ const handleCreate = async () => {
|
|||||||
closeCreateModal()
|
closeCreateModal()
|
||||||
actionError.value = ''
|
actionError.value = ''
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error('Failed to save area', err)
|
console.error('Failed to save area', err)
|
||||||
actionError.value = 'Errore durante il salvataggio dell\'area.'
|
|
||||||
|
const backendError = err?.data?.error || err?.response?.data?.error || err?.response?._data?.error;
|
||||||
|
|
||||||
|
if (backendError === 'area_outside_allowed_boundary') {
|
||||||
|
modalError.value = "Attenzione: l'area disegnata esce dai confini consentiti per il Comune di Bari.";
|
||||||
|
} else {
|
||||||
|
modalError.value = err?.data?.message || err?.message || "Errore durante il salvataggio dell'area.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,7 +394,7 @@ const handleDeleteArea = async () => {
|
|||||||
<div>
|
<div>
|
||||||
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm me-2">Torna al pannello admin</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">
|
<button class="btn btn-primary btn-sm" @click="openCreateModal">
|
||||||
<BIIcon name="it-plus" size="xs" class="me-1" />
|
<BIIcon class="me-1" name="it-plus" size="xs"/>
|
||||||
Nuova Area
|
Nuova Area
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -441,7 +456,8 @@ const handleDeleteArea = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<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-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 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-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -450,12 +466,19 @@ const handleDeleteArea = async () => {
|
|||||||
<button type="button" class="btn-close" @click="closeCreateModal"></button>
|
<button type="button" class="btn-close" @click="closeCreateModal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<!-- ALERT ERRORE DEL BACKEND -->
|
||||||
|
<div v-if="modalError" class="alert alert-danger mb-4 d-flex align-items-center" role="alert">
|
||||||
|
<strong>Errore:</strong> {{ modalError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="handleCreate">
|
<form @submit.prevent="handleCreate">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Nome Area</label>
|
<label class="form-label">Nome Area</label>
|
||||||
<input type="text" class="form-control" v-model="formData.name" required placeholder="Es. Centro Storico" />
|
<input v-model="formData.name" class="form-control" placeholder="Es. Centro Storico" required
|
||||||
|
type="text"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Descrizione (opzionale)</label>
|
<label class="form-label">Descrizione (opzionale)</label>
|
||||||
@@ -483,7 +506,8 @@ const handleDeleteArea = async () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-light border small mb-2 p-2">
|
<div class="alert alert-light border small mb-2 p-2">
|
||||||
Clicca sulla mappa per aggiungere vertici al poligono. Sono necessari almeno 3 punti. Sono ammessi solo punti nel Comune di Bari.
|
Clicca sulla mappa per aggiungere vertici al poligono. Sono necessari almeno 3 punti. Sono ammessi
|
||||||
|
solo punti nel Comune di Bari.
|
||||||
</div>
|
</div>
|
||||||
<div id="areaMap" class="rounded border" style="height: 400px; width: 100%; z-index: 1;"></div>
|
<div id="areaMap" class="rounded border" style="height: 400px; width: 100%; z-index: 1;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -492,7 +516,8 @@ const handleDeleteArea = async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer d-flex justify-content-end gap-2">
|
<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-outline-secondary" @click="closeCreateModal">Annulla</button>
|
||||||
<button type="button" class="btn btn-primary" :disabled="!formData.name || !formData.geojson" @click="handleCreate">
|
<button :disabled="!formData.name || !formData.geojson" class="btn btn-primary" type="button"
|
||||||
|
@click="handleCreate">
|
||||||
{{ submitLabel }}
|
{{ submitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -80,12 +80,11 @@ const handleCieLogin = async () => {
|
|||||||
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary w-100 mb-4 d-flex align-items-center justify-content-center gap-2"
|
class="btn btn-primary w-100 mb-4 d-flex align-items-center justify-content-center gap-2 entraconcie"
|
||||||
:disabled="isCieLoading || isLoading"
|
:disabled="isCieLoading || isLoading"
|
||||||
@click="handleCieLogin"
|
@click="handleCieLogin"
|
||||||
>
|
>
|
||||||
<span v-if="isCieLoading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
<span v-if="isCieLoading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
<BIIcon name="it-card" size="sm" color="white" />
|
|
||||||
Entra con CIE
|
Entra con CIE
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -147,4 +146,12 @@ const handleCieLogin = async () => {
|
|||||||
.pe-5 {
|
.pe-5 {
|
||||||
padding-right: 3rem !important;
|
padding-right: 3rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.entraconcie {
|
||||||
|
background-image: url('/entraconcie.png');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
background-size: 100%;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+3
-1
@@ -3,6 +3,8 @@ import BIIcon from "~/components/BIIcon.vue";
|
|||||||
|
|
||||||
const { getOpenDataReports } = usePublic()
|
const { getOpenDataReports } = usePublic()
|
||||||
|
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
total_reports: 14523,
|
total_reports: 14523,
|
||||||
resolved_reports: 13890,
|
resolved_reports: 13890,
|
||||||
@@ -38,7 +40,7 @@ const getStatusBadge = (status: string) => {
|
|||||||
e monitora l'intervento in totale trasparenza.
|
e monitora l'intervento in totale trasparenza.
|
||||||
</p>
|
</p>
|
||||||
<div class="it-btn-container d-flex justify-content-center gap-3">
|
<div class="it-btn-container d-flex justify-content-center gap-3">
|
||||||
<NuxtLink class="btn btn-primary" to="/auth/login">
|
<NuxtLink v-if="isCitizen(user.value?.role)" class="btn btn-primary" to="/auth/login">
|
||||||
Fai una segnalazione
|
Fai una segnalazione
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink class="btn btn-outline-primary" to="/open-data">
|
<NuxtLink class="btn btn-outline-primary" to="/open-data">
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ const initMap = async () => {
|
|||||||
watch(currentView, async (view) => {
|
watch(currentView, async (view) => {
|
||||||
if (view !== 'map') return
|
if (view !== 'map') return
|
||||||
await nextTick()
|
await nextTick()
|
||||||
// Inizializza la mappa se non esiste, altrimenti aggiornane solo dimensioni e dati
|
|
||||||
if (!map) {
|
if (!map) {
|
||||||
await initMap()
|
await initMap()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ const formatDate = (dateString: string) =>
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="h6 card-title mb-3">Azioni account</h2>
|
<h2 class="h6 card-title mb-3">Azioni account</h2>
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<NuxtLink to="/reports/new" class="btn btn-primary">Nuova segnalazione</NuxtLink>
|
<NuxtLink v-if="isCitizen(user?.role)" class="btn btn-primary" to="/reports/new">Nuova segnalazione
|
||||||
|
</NuxtLink>
|
||||||
<NuxtLink to="/notifications" class="btn btn-outline-primary">Notifiche</NuxtLink>
|
<NuxtLink to="/notifications" class="btn btn-outline-primary">Notifiche</NuxtLink>
|
||||||
<button type="button" class="btn btn-outline-danger" @click="logout">
|
<button type="button" class="btn btn-outline-danger" @click="logout">
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Reference in New Issue
Block a user