style(admin): improve moderation UI and catalog button alignment

Added dismissed card styling and reorganized error/empty state rendering in moderation. Added fixed width for catalog action buttons.
This commit is contained in:
2026-06-16 18:30:56 +02:00 Unverified
parent 220f613309
commit 26733b74de
2 changed files with 87 additions and 43 deletions
+7
View File
@@ -444,3 +444,10 @@ const handleDeleteTeam = async () => {
/> />
</div> </div>
</template> </template>
<style>
.text-end .btn {
margin: 0 !important;
width: 120px;
}
</style>
+80 -43
View File
@@ -28,9 +28,9 @@ const dismissModal = reactive({
}) })
const { data: flagsRes, pending, refresh } = useAsyncData( const { data: flagsRes, pending, refresh } = useAsyncData(
'admin-moderation-flags', 'admin-moderation-flags',
() => getModerationFlags({ page: page.value, per_page: perPage.value, status: statusFilter.value || undefined }), () => getModerationFlags({ page: page.value, per_page: perPage.value, status: statusFilter.value || undefined }),
{ watch: [page, statusFilter] } { watch: [page, statusFilter] }
) )
const openResolveModal = (id: number, rejectReport: boolean) => { const openResolveModal = (id: number, rejectReport: boolean) => {
@@ -54,15 +54,14 @@ const handleResolve = async () => {
actionError.value = '' actionError.value = ''
try { try {
const res = await updateModerationFlag(resolveModal.flagId, { await updateModerationFlag(resolveModal.flagId, {
status: 'RESOLVED', status: 'RESOLVED',
reject_report: resolveModal.rejectReport, reject_report: resolveModal.rejectReport,
resolution_note: resolveModal.note || undefined resolution_note: resolveModal.note || undefined
}) })
if (res.ok) {
await refresh() await refresh()
closeResolveModal() closeResolveModal()
}
} catch (err) { } catch (err) {
console.error('Failed to resolve flag', err) console.error('Failed to resolve flag', err)
actionError.value = 'Errore durante la risoluzione della segnalazione.' actionError.value = 'Errore durante la risoluzione della segnalazione.'
@@ -89,11 +88,10 @@ const handleDismiss = async () => {
actionError.value = '' actionError.value = ''
try { try {
const res = await updateModerationFlag(dismissModal.flagId, { status: 'DISMISSED' }) await updateModerationFlag(dismissModal.flagId, { status: 'DISMISSED' })
if (res.ok) {
await refresh() await refresh()
closeDismissModal() closeDismissModal()
}
} catch (err) { } catch (err) {
console.error('Failed to dismiss flag', err) console.error('Failed to dismiss flag', err)
actionError.value = 'Errore durante l\'aggiornamento.' actionError.value = 'Errore durante l\'aggiornamento.'
@@ -103,7 +101,7 @@ const handleDismiss = async () => {
} }
const formatDate = (dateStr: string) => { const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleString('it-IT', { return new Date(dateStr).toLocaleString('it-IT', {
day: '2-digit', month: '2-digit', year: 'numeric', day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit' hour: '2-digit', minute: '2-digit'
}) })
@@ -129,23 +127,30 @@ const formatDate = (dateStr: string) => {
</div> </div>
</div> </div>
<!-- Spostato in modo che non interrompa l'if-else dei caricamenti -->
<div v-if="actionError" class="alert alert-danger mb-4">
{{ actionError }}
</div>
<div v-if="pending" class="text-center py-5"> <div v-if="pending" class="text-center py-5">
<div class="spinner-border text-danger" role="status"></div> <div class="spinner-border text-danger" role="status"></div>
</div> </div>
<div v-else-if="!flagsRes?.flags || flagsRes.flags.length === 0" class="alert alert-success"> <div v-else-if="!flagsRes || !flagsRes.flags || flagsRes.flags.length === 0" class="alert alert-success">
Nessun elemento da moderare al momento. Nessun elemento da moderare al momento.
</div> </div>
<div v-if="actionError" class="alert alert-danger"> <!-- Questo v-else ora è scatenato in modo pulito solo quando i dati esistono davvero -->
{{ actionError }}
</div>
<div v-else> <div v-else>
<div class="card mb-3 shadow-sm border-0" v-for="flag in flagsRes.flags" :key="flag.id"> <div
<div class="card-header bg-light d-flex justify-content-between align-items-center"> 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> <div>
<strong>Flag #{{ flag.id }}</strong> - <strong>Flag #{{ flag.id }}</strong> -
<span class="badge" :class="{ <span class="badge" :class="{
'bg-warning': flag.status === 'OPEN', 'bg-warning': flag.status === 'OPEN',
'bg-success': flag.status === 'RESOLVED', 'bg-success': flag.status === 'RESOLVED',
@@ -161,8 +166,8 @@ const formatDate = (dateStr: string) => {
<p class="card-text text-muted mb-2"> <p class="card-text text-muted mb-2">
{{ flag.details || 'Nessun dettaglio aggiuntivo fornito.' }} {{ flag.details || 'Nessun dettaglio aggiuntivo fornito.' }}
</p> </p>
<div class="mt-3"> <div class="mt-info">
<span class="badge bg-info me-2">{{ flag.content_type }}</span> <span class="badge bg-info me-2">{{ flag.content_type }}</span>
<span v-if="flag.report_id"> <span v-if="flag.report_id">
Rif: Rif:
@@ -174,17 +179,17 @@ const formatDate = (dateStr: string) => {
| Messaggio #{{ flag.message_id }} | Messaggio #{{ flag.message_id }}
</span> </span>
</div> </div>
<div class="mt-3 small"> <div class="mt-info small text-muted">
Segnalato da: {{ flag.created_by_email }} (ID: {{ flag.created_by_user_id }}) Segnalato da: {{ flag.created_by_email }} (ID: {{ flag.created_by_user_id }})
</div> </div>
<div v-if="flag.status !== 'OPEN'" class="mt-2 small text-muted border-top pt-2"> <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' }} <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 v-if="flag.resolution_note"><strong>Nota:</strong> {{ flag.resolution_note }}</div>
</div> </div>
</div> </div>
<div class="col-md-4 text-end" v-if="flag.status === 'OPEN'"> <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"> <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, true)"> <button class="btn btn-outline-danger btn-sm" @click="openResolveModal(flag.id, true)">
@@ -202,12 +207,12 @@ const formatDate = (dateStr: string) => {
</div> </div>
</div> </div>
<Pagination <Pagination
v-if="(flagsRes.total || flagsRes.flags.length) > perPage" v-if="(flagsRes.total || flagsRes.flags.length) > perPage"
:current-page="page" :current-page="page"
:per-page="perPage" :per-page="perPage"
:total-items="flagsRes.total || (page * perPage + 1)" :total-items="flagsRes.total || (page * perPage + 1)"
@page-change="page = $event" @page-change="page = $event"
/> />
</div> </div>
@@ -235,14 +240,46 @@ const formatDate = (dateStr: string) => {
</div> </div>
<ConfirmModal <ConfirmModal
:open="dismissModal.open" :open="dismissModal.open"
title="Ignora flag" title="Ignora flag"
message="Vuoi ignorare questo flag? Azione registrata in moderazione." message="Vuoi ignorare questo flag? Azione registrata in moderazione."
confirm-label="Ignora flag" confirm-label="Ignora flag"
confirm-class="btn-outline-secondary" confirm-class="btn-outline-secondary"
:pending="dismissModal.pending" :pending="dismissModal.pending"
@cancel="closeDismissModal" @cancel="closeDismissModal"
@confirm="handleDismiss" @confirm="handleDismiss"
/> />
</div> </div>
</template> </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>