297 lines
9.6 KiB
Vue
297 lines
9.6 KiB
Vue
<script setup lang="ts">
|
|
import { reactive, ref } 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 actionError = ref('')
|
|
const resolveModal = reactive({
|
|
open: false,
|
|
flagId: null as number | null,
|
|
rejectReport: false,
|
|
note: '',
|
|
pending: false,
|
|
})
|
|
const dismissModal = reactive({
|
|
open: false,
|
|
flagId: null as number | null,
|
|
pending: false,
|
|
})
|
|
|
|
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 openResolveModal = (id: number, rejectReport: boolean) => {
|
|
resolveModal.open = true
|
|
resolveModal.flagId = id
|
|
resolveModal.rejectReport = rejectReport
|
|
resolveModal.note = ''
|
|
}
|
|
|
|
const closeResolveModal = () => {
|
|
if (resolveModal.pending) return
|
|
resolveModal.open = false
|
|
resolveModal.flagId = null
|
|
resolveModal.note = ''
|
|
}
|
|
|
|
const handleResolve = async () => {
|
|
if (!resolveModal.flagId) return
|
|
|
|
resolveModal.pending = true
|
|
actionError.value = ''
|
|
|
|
try {
|
|
await updateModerationFlag(resolveModal.flagId, {
|
|
status: 'RESOLVED',
|
|
reject_report: resolveModal.rejectReport,
|
|
resolution_note: resolveModal.note || undefined
|
|
})
|
|
|
|
await refresh()
|
|
closeResolveModal()
|
|
} catch (err) {
|
|
console.error('Failed to resolve flag', err)
|
|
actionError.value = 'Errore durante la risoluzione della segnalazione.'
|
|
} finally {
|
|
resolveModal.pending = false
|
|
}
|
|
}
|
|
|
|
const openDismissModal = (id: number) => {
|
|
dismissModal.open = true
|
|
dismissModal.flagId = id
|
|
}
|
|
|
|
const closeDismissModal = () => {
|
|
if (dismissModal.pending) return
|
|
dismissModal.open = false
|
|
dismissModal.flagId = null
|
|
}
|
|
|
|
const handleDismiss = async () => {
|
|
if (!dismissModal.flagId) return
|
|
|
|
dismissModal.pending = true
|
|
actionError.value = ''
|
|
|
|
try {
|
|
await updateModerationFlag(dismissModal.flagId, { status: 'DISMISSED' })
|
|
|
|
await refresh()
|
|
closeDismissModal()
|
|
} catch (err) {
|
|
console.error('Failed to dismiss flag', err)
|
|
actionError.value = 'Errore durante l\'aggiornamento.'
|
|
} finally {
|
|
dismissModal.pending = false
|
|
}
|
|
}
|
|
|
|
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'
|
|
})
|
|
}
|
|
|
|
const formatFlagStatus = (status: string) => {
|
|
const map: Record<string, string> = {
|
|
OPEN: 'Aperto',
|
|
RESOLVED: 'Risolto',
|
|
DISMISSED: 'Ignorato'
|
|
}
|
|
|
|
return map[status] || status
|
|
}
|
|
|
|
const contentTypeLabel = (type: string) => type === 'REPORT' ? 'Segnalazione' : 'Messaggio'
|
|
const canRejectReport = (flag: any) => flag.content_type === 'REPORT'
|
|
</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 al pannello admin</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="actionError" class="alert alert-danger mb-4">
|
|
{{ actionError }}
|
|
</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 || !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 border-0"
|
|
:class="flag.status === 'DISMISSED' ? 'dismissed-card shadow-none' : 'shadow-sm'"
|
|
v-for="flag in flagsRes.flags"
|
|
:key="flag.id"
|
|
>
|
|
<div class="card-header d-flex justify-content-between align-items-center" :class="flag.status === 'DISMISSED' ? 'bg-transparent' : 'bg-light'">
|
|
<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'
|
|
}">{{ formatFlagStatus(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-info">
|
|
<span class="badge bg-info me-2">{{ contentTypeLabel(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-info small text-muted">
|
|
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="openResolveModal(flag.id, canRejectReport(flag))">
|
|
<BIIcon name="it-ban" size="xs" /> {{ canRejectReport(flag) ? 'Risolvi & Rigetta Segn.' : 'Risolvi flag' }}
|
|
</button>
|
|
<button class="btn btn-outline-warning btn-sm" @click="openResolveModal(flag.id, false)">
|
|
<BIIcon name="it-check" size="xs" /> {{ canRejectReport(flag) ? 'Risolvi (Solo Avviso)' : 'Risolvi senza rigetto' }}
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" @click="openDismissModal(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 v-if="resolveModal.open" class="modal-backdrop fade show d-block" style="background: rgba(0,0,0,0.5); z-index: 1040;"></div>
|
|
<div v-if="resolveModal.open" class="modal fade show d-block" tabindex="-1" style="z-index: 1050;">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">{{ resolveModal.rejectReport ? 'Risolvi e rigetta segnalazione' : 'Risolvi flag' }}</h5>
|
|
<button type="button" class="btn-close" :disabled="resolveModal.pending" @click="closeResolveModal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<label class="form-label">Nota di risoluzione</label>
|
|
<textarea v-model="resolveModal.note" class="form-control" rows="4" placeholder="Nota opzionale"></textarea>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" :disabled="resolveModal.pending" @click="closeResolveModal">Annulla</button>
|
|
<button type="button" class="btn btn-primary" :disabled="resolveModal.pending" @click="handleResolve">
|
|
<span v-if="resolveModal.pending" class="spinner-border spinner-border-sm me-2"></span>
|
|
Conferma
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<ConfirmModal
|
|
:open="dismissModal.open"
|
|
title="Ignora flag"
|
|
message="Vuoi ignorare questo flag? Azione registrata in moderazione."
|
|
confirm-label="Ignora flag"
|
|
confirm-class="btn-outline-secondary"
|
|
:pending="dismissModal.pending"
|
|
@cancel="closeDismissModal"
|
|
@confirm="handleDismiss"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.dismissed-card {
|
|
background-color: #f8f9fa;
|
|
opacity: 0.7;
|
|
border: 1px solid #e9ecef !important;
|
|
}
|
|
|
|
.dismissed-card .card-header {
|
|
padding-top: 0.35rem;
|
|
padding-bottom: 0.35rem;
|
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.dismissed-card .card-body {
|
|
padding-top: 0.6rem;
|
|
padding-bottom: 0.6rem;
|
|
}
|
|
|
|
.dismissed-card h5.card-title {
|
|
font-size: 1rem;
|
|
margin-bottom: 0.2rem;
|
|
}
|
|
|
|
.mt-info {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.dismissed-card .mt-info {
|
|
margin-top: 0.4rem;
|
|
}
|
|
</style>
|