feat(admin): implement admin dashboard pages for users, moderation and areas
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
auth: { role: 'ADMIN' }
|
||||
})
|
||||
|
||||
useHead({ title: 'Gestione Utenti - Admin' })
|
||||
|
||||
const { getUsers, updateUserBan, updateUserRoles } = useAdmin()
|
||||
|
||||
const { data: usersRes, pending, refresh } = useAsyncData('admin-users', () => getUsers())
|
||||
|
||||
const search = ref('')
|
||||
|
||||
const filteredUsers = computed(() => {
|
||||
if (!usersRes.value?.users) return []
|
||||
if (!search.value) return usersRes.value.users
|
||||
const s = search.value.toLowerCase()
|
||||
return usersRes.value.users.filter(u =>
|
||||
u.first_name.toLowerCase().includes(s) ||
|
||||
u.last_name.toLowerCase().includes(s) ||
|
||||
u.email.toLowerCase().includes(s) ||
|
||||
u.fiscal_code?.toLowerCase().includes(s)
|
||||
)
|
||||
})
|
||||
|
||||
const handleToggleBan = async (id: number, currentBanStatus: boolean) => {
|
||||
if (!confirm(`Sei sicuro di voler ${currentBanStatus ? 'sbloccare' : 'bannare'} questo utente?`)) return
|
||||
|
||||
try {
|
||||
const res = await updateUserBan(id, { banned: !currentBanStatus })
|
||||
if (res.ok) {
|
||||
await refresh()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update user ban status', err)
|
||||
alert('Errore durante l\'aggiornamento dello stato di ban.')
|
||||
}
|
||||
}
|
||||
|
||||
// Modal per i ruoli
|
||||
const editingUserId = ref<number | null>(null)
|
||||
const selectedRoles = ref<string[]>([])
|
||||
const availableRoles = ['CITIZEN', 'OPERATOR', 'ADMIN']
|
||||
|
||||
const openRoleModal = (user: any) => {
|
||||
editingUserId.value = user.id
|
||||
selectedRoles.value = user.roles ? user.roles.split(',').map((r: string) => r.trim()) : []
|
||||
}
|
||||
|
||||
const closeRoleModal = () => {
|
||||
editingUserId.value = null
|
||||
selectedRoles.value = []
|
||||
}
|
||||
|
||||
const saveRoles = async () => {
|
||||
if (!editingUserId.value) return
|
||||
|
||||
try {
|
||||
const res = await updateUserRoles(editingUserId.value, { roles: selectedRoles.value })
|
||||
if (res.ok) {
|
||||
await refresh()
|
||||
closeRoleModal()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update roles', err)
|
||||
alert('Errore durante l\'aggiornamento dei ruoli.')
|
||||
}
|
||||
}
|
||||
</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 Utenti</h1>
|
||||
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm">Torna alla Dashboard</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="Cerca per nome, email o CF..." v-model="search">
|
||||
</div>
|
||||
</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="!usersRes?.users || usersRes.users.length === 0" class="alert alert-info">
|
||||
Nessun utente trovato.
|
||||
</div>
|
||||
|
||||
<div v-else class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nome</th>
|
||||
<th>Email / CF</th>
|
||||
<th>Ruoli</th>
|
||||
<th>Punti</th>
|
||||
<th>Stato</th>
|
||||
<th class="text-end">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in filteredUsers" :key="user.id">
|
||||
<td>{{ user.id }}</td>
|
||||
<td>
|
||||
<strong>{{ user.first_name }} {{ user.last_name }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ user.email }}</div>
|
||||
<small class="text-muted">{{ user.fiscal_code }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span v-for="r in (user.roles ? user.roles.split(',') : [])" :key="r" class="badge bg-primary me-1">
|
||||
{{ r.trim() }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ user.points || 0 }}</td>
|
||||
<td>
|
||||
<span v-if="user.banned" class="badge bg-danger">Bannato</span>
|
||||
<span v-else class="badge bg-success">Attivo</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary me-2"
|
||||
@click="openRoleModal(user)"
|
||||
>
|
||||
Ruoli
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
:class="user.banned ? 'btn-outline-success' : 'btn-outline-danger'"
|
||||
@click="handleToggleBan(user.id, !!user.banned)"
|
||||
>
|
||||
{{ user.banned ? 'Sblocca' : 'Banna' }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="editingUserId !== null" class="modal d-block" tabindex="-1" style="background: rgba(0,0,0,0.5)">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Modifica Ruoli Utente</h5>
|
||||
<button type="button" class="btn-close" aria-label="Close" @click="closeRoleModal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-check mb-2" v-for="role in availableRoles" :key="role">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
:value="role"
|
||||
:id="'role-'+role"
|
||||
v-model="selectedRoles"
|
||||
>
|
||||
<label class="form-check-label" :for="'role-'+role">
|
||||
{{ role }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="closeRoleModal">Annulla</button>
|
||||
<button type="button" class="btn btn-primary" @click="saveRoles">Salva</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user