feat(admin): implement admin dashboard pages for users, moderation and areas
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
auth: { role: 'ADMIN' }
|
||||
})
|
||||
|
||||
useHead({ title: 'Moderazione Abusi - Admin' })
|
||||
|
||||
const { getModerationFlags, updateModerationFlag } = useAdmin()
|
||||
|
||||
const page = ref(1)
|
||||
const perPage = ref(10)
|
||||
const statusFilter = ref('')
|
||||
|
||||
const { data: flagsRes, pending, refresh } = useAsyncData(
|
||||
'admin-moderation-flags',
|
||||
() => getModerationFlags({ page: page.value, per_page: perPage.value, status: statusFilter.value || undefined }),
|
||||
{ watch: [page, statusFilter] }
|
||||
)
|
||||
|
||||
const handleResolve = async (id: number, rejectReport: boolean) => {
|
||||
const note = prompt('Inserisci una nota di risoluzione (opzionale):')
|
||||
if (note === null) return // cancelled
|
||||
|
||||
try {
|
||||
const res = await updateModerationFlag(id, {
|
||||
status: 'RESOLVED',
|
||||
reject_report: rejectReport,
|
||||
resolution_note: note
|
||||
})
|
||||
if (res.ok) {
|
||||
await refresh()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to resolve flag', err)
|
||||
alert('Errore durante la risoluzione della segnalazione.')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDismiss = async (id: number) => {
|
||||
if (!confirm('Sei sicuro di voler ignorare (dismiss) questo flag?')) return
|
||||
|
||||
try {
|
||||
const res = await updateModerationFlag(id, { status: 'DISMISSED' })
|
||||
if (res.ok) {
|
||||
await refresh()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to dismiss flag', err)
|
||||
alert('Errore durante l\'aggiornamento.')
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString('it-IT', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
})
|
||||
}
|
||||
</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">Moderazione Abusi</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-4">
|
||||
<label class="form-label">Filtra per Stato</label>
|
||||
<select class="form-select" v-model="statusFilter">
|
||||
<option value="">Tutti</option>
|
||||
<option value="OPEN">Aperti</option>
|
||||
<option value="RESOLVED">Risolti</option>
|
||||
<option value="DISMISSED">Ignorati</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="pending" class="text-center py-5">
|
||||
<div class="spinner-border text-danger" role="status"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!flagsRes?.flags || flagsRes.flags.length === 0" class="alert alert-success">
|
||||
Nessun elemento da moderare al momento.
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="card mb-3 shadow-sm border-0" v-for="flag in flagsRes.flags" :key="flag.id">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>Flag #{{ flag.id }}</strong> -
|
||||
<span class="badge" :class="{
|
||||
'bg-warning': flag.status === 'OPEN',
|
||||
'bg-success': flag.status === 'RESOLVED',
|
||||
'bg-secondary': flag.status === 'DISMISSED'
|
||||
}">{{ flag.status }}</span>
|
||||
</div>
|
||||
<small class="text-muted">{{ formatDate(flag.created_at) }}</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h5 class="card-title">Motivo: {{ flag.reason }}</h5>
|
||||
<p class="card-text text-muted mb-2">
|
||||
{{ flag.details || 'Nessun dettaglio aggiuntivo fornito.' }}
|
||||
</p>
|
||||
|
||||
<div class="mt-3">
|
||||
<span class="badge bg-info me-2">{{ flag.content_type }}</span>
|
||||
<span v-if="flag.report_id">
|
||||
Rif:
|
||||
<NuxtLink :to="`/operator/reports/${flag.report_id}`" class="text-decoration-none">
|
||||
Segnalazione #{{ flag.report_id }}
|
||||
</NuxtLink>
|
||||
</span>
|
||||
<span v-if="flag.message_id" class="ms-2">
|
||||
| Messaggio #{{ flag.message_id }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 small">
|
||||
Segnalato da: {{ flag.created_by_email }} (ID: {{ flag.created_by_user_id }})
|
||||
</div>
|
||||
|
||||
<div v-if="flag.status !== 'OPEN'" class="mt-2 small text-muted border-top pt-2">
|
||||
<strong>Risolto/Ignorato da:</strong> {{ flag.resolved_by_email }} in data {{ flag.resolved_at ? formatDate(flag.resolved_at) : 'N/D' }}
|
||||
<div v-if="flag.resolution_note"><strong>Nota:</strong> {{ flag.resolution_note }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 text-end" v-if="flag.status === 'OPEN'">
|
||||
<div class="d-flex flex-column gap-2 mt-3 mt-md-0">
|
||||
<button class="btn btn-outline-danger btn-sm" @click="handleResolve(flag.id, true)">
|
||||
<BIIcon name="it-ban" size="xs" /> Risolvi & Rigetta Segn.
|
||||
</button>
|
||||
<button class="btn btn-outline-warning btn-sm" @click="handleResolve(flag.id, false)">
|
||||
<BIIcon name="it-check" size="xs" /> Risolvi (Solo Avviso)
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @click="handleDismiss(flag.id)">
|
||||
<BIIcon name="it-close" size="xs" /> Ignora Flag
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
v-if="(flagsRes.total || flagsRes.flags.length) > perPage"
|
||||
:current-page="page"
|
||||
:per-page="perPage"
|
||||
:total-items="flagsRes.total || (page * perPage + 1)"
|
||||
@page-change="page = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user