Initialer Laravel Commit für BetiX
This commit is contained in:
761
resources/js/pages/Admin/ChatReportShow.vue
Normal file
761
resources/js/pages/Admin/ChatReportShow.vue
Normal file
@@ -0,0 +1,761 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, router } from '@inertiajs/vue3';
|
||||
import { ref } from 'vue';
|
||||
import CasinoAdminLayout from '@/layouts/admin/CasinoAdminLayout.vue';
|
||||
import {
|
||||
ArrowLeft, Flag, Hash, Clock, CheckCircle2, XCircle,
|
||||
Ban, MessageSquareOff, AlertTriangle, History,
|
||||
Calendar, Mail, Shield, Gavel, Loader2, ChevronRight,
|
||||
UserRound, ShieldAlert, Star, Crown, ArrowRight
|
||||
} from 'lucide-vue-next';
|
||||
|
||||
interface Restriction {
|
||||
id: number;
|
||||
type: 'chat_ban' | 'account_ban';
|
||||
reason: string | null;
|
||||
active: boolean;
|
||||
starts_at: string | null;
|
||||
ends_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface UserProfile {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
avatar: string | null;
|
||||
avatar_url: string | null;
|
||||
role: string;
|
||||
vip_level: number;
|
||||
is_banned: boolean;
|
||||
created_at: string;
|
||||
restrictions?: Restriction[];
|
||||
}
|
||||
|
||||
interface ContextMsg {
|
||||
id: string | number;
|
||||
message: string;
|
||||
user: { id: number; username: string };
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface Report {
|
||||
id: number;
|
||||
reporter_id: number;
|
||||
message_id: string;
|
||||
message_text: string;
|
||||
sender_id: number | null;
|
||||
sender_username: string | null;
|
||||
reason: string | null;
|
||||
context_messages: ContextMsg[] | null;
|
||||
status: 'pending' | 'reviewed' | 'dismissed';
|
||||
admin_note: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
report: Report;
|
||||
senderUser: UserProfile | null;
|
||||
reporterUser: UserProfile | null;
|
||||
flash?: string | null;
|
||||
}>();
|
||||
|
||||
// ── Restriction management ────────────────────────────────────
|
||||
const extendHours = ref<Record<number, number>>({});
|
||||
const flashMsg = ref(props.flash || '');
|
||||
|
||||
function liftRestriction(id: number) {
|
||||
router.post(`/admin/restrictions/${id}/lift`, {}, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => { flashMsg.value = 'Sperre wurde aufgehoben.'; },
|
||||
});
|
||||
}
|
||||
function extendRestriction(id: number) {
|
||||
const h = extendHours.value[id];
|
||||
if (!h || h < 1) return;
|
||||
router.post(`/admin/restrictions/${id}/extend`, { hours: h }, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => { flashMsg.value = `Sperre um ${h}h verlängert.`; extendHours.value[id] = 0; },
|
||||
});
|
||||
}
|
||||
|
||||
// ── Punishment ────────────────────────────────────────────────
|
||||
const punishType = ref<'chat_ban' | 'account_ban'>('chat_ban');
|
||||
const punishReason = ref('');
|
||||
const punishHours = ref<number | null>(null);
|
||||
const submitting = ref(false);
|
||||
|
||||
const chatTemplates = [
|
||||
{ label: '1 Tag', hours: 24, reason: 'Spam' },
|
||||
{ label: '3 Tage', hours: 72, reason: 'Beleidigung' },
|
||||
{ label: '7 Tage', hours: 168, reason: 'Belästigung' },
|
||||
{ label: '30 Tage', hours: 720, reason: 'Schwerer Verstoß' },
|
||||
{ label: 'Permanent',hours: null, reason: 'Dauerhafter Ausschluss' },
|
||||
];
|
||||
const banTemplates = [
|
||||
{ label: '1 Tag', hours: 24, reason: 'Betrug / Scam' },
|
||||
{ label: '7 Tage', hours: 168, reason: 'Schwerer Verstoß' },
|
||||
{ label: '30 Tage', hours: 720, reason: 'Wiederholte Verstöße' },
|
||||
{ label: 'Permanent',hours: null, reason: 'Dauerhafter Ausschluss' },
|
||||
];
|
||||
|
||||
function applyTemplate(reason: string, hours: number | null) {
|
||||
punishReason.value = reason;
|
||||
punishHours.value = hours;
|
||||
}
|
||||
|
||||
function submitPunish() {
|
||||
if (!punishReason.value || submitting.value) return;
|
||||
submitting.value = true;
|
||||
router.post(`/admin/reports/chat/${props.report.id}/punish`, {
|
||||
type: punishType.value, reason: punishReason.value, hours: punishHours.value,
|
||||
}, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => { flashMsg.value = 'Strafe erfolgreich verhängt!'; },
|
||||
onFinish: () => { submitting.value = false; },
|
||||
});
|
||||
}
|
||||
|
||||
function updateStatus(status: string) {
|
||||
router.post(`/admin/reports/chat/${props.report.id}`, { status }, { preserveScroll: true });
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────
|
||||
const statusMeta: Record<string, { color: string; bg: string; border: string; label: string }> = {
|
||||
pending: { color: '#f59e0b', bg: 'rgba(245,158,11,0.12)', border: 'rgba(245,158,11,0.35)', label: 'Ausstehend' },
|
||||
reviewed: { color: '#22c55e', bg: 'rgba(34,197,94,0.12)', border: 'rgba(34,197,94,0.35)', label: 'Bearbeitet' },
|
||||
dismissed: { color: '#6b7280', bg: 'rgba(107,114,128,0.1)', border: 'rgba(107,114,128,0.3)', label: 'Abgelehnt' },
|
||||
};
|
||||
const reasonLabels: Record<string, string> = {
|
||||
spam:'Spam', beleidigung:'Beleidigung', belaestigung:'Belästigung', betrug:'Betrug', sonstiges:'Sonstiges',
|
||||
};
|
||||
function rl(r: string | null) { return r ? (reasonLabels[r] ?? r) : null; }
|
||||
|
||||
function avUrl(u: UserProfile | null) { return u?.avatar_url || u?.avatar || null; }
|
||||
function ini(name?: string | null) { return (name || '?')[0].toUpperCase(); }
|
||||
|
||||
function fmt(d: string | null) {
|
||||
if (!d) return '–';
|
||||
return new Date(d).toLocaleString('de-DE', { day:'2-digit', month:'2-digit', year:'2-digit', hour:'2-digit', minute:'2-digit' });
|
||||
}
|
||||
function fmtDur(h: number | null) {
|
||||
if (h === null) return 'Permanent';
|
||||
if (h < 24) return `${h}h`;
|
||||
return `${h/24}d`;
|
||||
}
|
||||
function timeLeft(d: string | null) {
|
||||
if (!d) return 'Permanent';
|
||||
const ms = new Date(d).getTime() - Date.now();
|
||||
if (ms <= 0) return 'Abgelaufen';
|
||||
const days = Math.floor(ms / 86400000);
|
||||
const hrs = Math.floor((ms % 86400000) / 3600000);
|
||||
return days > 0 ? `${days}T ${hrs}h` : `${hrs}h`;
|
||||
}
|
||||
|
||||
function activeR(u: UserProfile | null) {
|
||||
return (u?.restrictions ?? []).filter(r => r.active && (!r.ends_at || new Date(r.ends_at) > new Date()));
|
||||
}
|
||||
function allR(u: UserProfile | null) { return u?.restrictions ?? []; }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CasinoAdminLayout>
|
||||
<Head :title="`Case #${report.id}`" />
|
||||
<template #title>
|
||||
<div class="pt">
|
||||
<a href="/admin/reports/chat" class="back"><ArrowLeft :size="14" /> Chat Reports</a>
|
||||
<span class="ptdiv">/</span>
|
||||
<span class="pt-case"><Hash :size="12" />{{ report.id }}</span>
|
||||
<span class="status-chip" :style="{ color: statusMeta[report.status].color, background: statusMeta[report.status].bg, borderColor: statusMeta[report.status].border }">
|
||||
{{ statusMeta[report.status].label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Flash banner -->
|
||||
<transition name="fade">
|
||||
<div v-if="flashMsg" class="flash">
|
||||
<CheckCircle2 :size="15" />
|
||||
<span>{{ flashMsg }}</span>
|
||||
<button @click="flashMsg = ''"><XCircle :size="14" /></button>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- Case summary bar -->
|
||||
<div class="summary-bar">
|
||||
<div class="sb-item">
|
||||
<span class="sb-label">Melder</span>
|
||||
<span class="sb-val blue">@{{ reporterUser?.username || '–' }}</span>
|
||||
</div>
|
||||
<ArrowRight :size="14" class="sb-arrow" />
|
||||
<div class="sb-item">
|
||||
<span class="sb-label">Gemeldet</span>
|
||||
<span class="sb-val pink">@{{ senderUser?.username || report.sender_username || '–' }}</span>
|
||||
</div>
|
||||
<div class="sb-sep" />
|
||||
<div class="sb-item" v-if="rl(report.reason)">
|
||||
<span class="sb-label">Grund</span>
|
||||
<span class="sb-val"><Flag :size="11" /> {{ rl(report.reason) }}</span>
|
||||
</div>
|
||||
<div class="sb-item">
|
||||
<span class="sb-label">Datum</span>
|
||||
<span class="sb-val">{{ fmt(report.created_at) }}</span>
|
||||
</div>
|
||||
<div class="sb-item">
|
||||
<span class="sb-label">Msg-ID</span>
|
||||
<span class="sb-val mono">#{{ report.message_id }}</span>
|
||||
</div>
|
||||
<!-- Quick status -->
|
||||
<div class="sb-status-btns">
|
||||
<button :class="['ssb', { active: report.status === 'reviewed' }]" @click="updateStatus('reviewed')">
|
||||
<CheckCircle2 :size="12" /> Erledigt
|
||||
</button>
|
||||
<button :class="['ssb dismiss', { active: report.status === 'dismissed' }]" @click="updateStatus('dismissed')">
|
||||
<XCircle :size="12" /> Ablehnen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main grid -->
|
||||
<div class="main-grid">
|
||||
|
||||
<!-- ══ LEFT col ══════════════════════════════════════ -->
|
||||
<div class="col-left">
|
||||
|
||||
<!-- Reporter -->
|
||||
<div class="user-card blue-top">
|
||||
<div class="uc-label blue"><UserRound :size="10" /> Melder</div>
|
||||
<div class="uc-row">
|
||||
<div class="uc-av blue">
|
||||
<img v-if="avUrl(reporterUser)" :src="avUrl(reporterUser)!" />
|
||||
<span v-else>{{ ini(reporterUser?.username) }}</span>
|
||||
</div>
|
||||
<div class="uc-info">
|
||||
<div class="uc-name">@{{ reporterUser?.username || '–' }}</div>
|
||||
<div class="uc-email"><Mail :size="9" /> {{ reporterUser?.email || '–' }}</div>
|
||||
<div class="uc-since"><Calendar :size="9" /> seit {{ reporterUser?.created_at ? new Date(reporterUser.created_at).toLocaleDateString('de-DE') : '–' }}</div>
|
||||
</div>
|
||||
<a v-if="reporterUser" :href="`/admin/users/${reporterUser.id}`" class="uc-open" title="Profil öffnen"><ChevronRight :size="14" /></a>
|
||||
</div>
|
||||
<div class="uc-badges">
|
||||
<span class="badge role"><Crown :size="8" /> {{ reporterUser?.role || 'user' }}</span>
|
||||
<span v-if="reporterUser?.vip_level && reporterUser.vip_level > 0" class="badge vip"><Star :size="8" /> VIP {{ reporterUser.vip_level }}</span>
|
||||
<span v-if="reporterUser?.is_banned" class="badge banned"><Ban :size="8" /> Gebannt</span>
|
||||
<span v-if="activeR(reporterUser).some(r => r.type==='chat_ban')" class="badge cbanned"><MessageSquareOff :size="8" /> Chat-Bann</span>
|
||||
</div>
|
||||
<!-- Mini restriction history -->
|
||||
<div v-if="allR(reporterUser).length" class="mini-hist">
|
||||
<div class="mh-title"><History :size="9" /> Historie <span class="mh-count">{{ allR(reporterUser).length }}</span></div>
|
||||
<div v-for="r in allR(reporterUser).slice(0,3)" :key="r.id" class="mh-row" :class="{ active: r.active }">
|
||||
<span class="mh-type" :class="r.type==='chat_ban'?'chat':'ban'">{{ r.type==='chat_ban'?'Chat':'Acc' }}</span>
|
||||
<span class="mh-reason">{{ r.reason || '–' }}</span>
|
||||
<span v-if="r.active" class="mh-live">●</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="reported-divider"><Flag :size="11" class="df" /> hat gemeldet</div>
|
||||
|
||||
<!-- Reported / Sender -->
|
||||
<div class="user-card pink-top">
|
||||
<div class="uc-label pink"><ShieldAlert :size="10" /> Gemeldet</div>
|
||||
<div class="uc-row">
|
||||
<div class="uc-av pink">
|
||||
<img v-if="avUrl(senderUser)" :src="avUrl(senderUser)!" />
|
||||
<span v-else>{{ ini(senderUser?.username ?? report.sender_username) }}</span>
|
||||
</div>
|
||||
<div class="uc-info">
|
||||
<div class="uc-name">@{{ senderUser?.username || report.sender_username || '–' }}</div>
|
||||
<div class="uc-email"><Mail :size="9" /> {{ senderUser?.email || `ID: ${report.sender_id || '–'}` }}</div>
|
||||
<div v-if="senderUser" class="uc-since"><Calendar :size="9" /> seit {{ new Date(senderUser.created_at).toLocaleDateString('de-DE') }}</div>
|
||||
</div>
|
||||
<a v-if="senderUser" :href="`/admin/users/${senderUser.id}`" class="uc-open" title="Profil öffnen"><ChevronRight :size="14" /></a>
|
||||
</div>
|
||||
<div class="uc-badges">
|
||||
<span v-if="senderUser" class="badge role"><Crown :size="8" /> {{ senderUser.role }}</span>
|
||||
<span v-if="senderUser?.vip_level && senderUser.vip_level > 0" class="badge vip"><Star :size="8" /> VIP {{ senderUser.vip_level }}</span>
|
||||
<span v-if="senderUser?.is_banned" class="badge banned"><Ban :size="8" /> Gebannt</span>
|
||||
<span v-if="activeR(senderUser).some(r => r.type==='chat_ban')" class="badge cbanned"><MessageSquareOff :size="8" /> Chat-Bann</span>
|
||||
</div>
|
||||
|
||||
<!-- Active restrictions alert -->
|
||||
<div v-if="activeR(senderUser).length" class="active-alert">
|
||||
<AlertTriangle :size="13" />
|
||||
<div>
|
||||
<div class="aa-title">{{ activeR(senderUser).length }} aktive Sperre(n)</div>
|
||||
<div class="aa-list">
|
||||
<div v-for="r in activeR(senderUser)" :key="r.id" class="aa-row">
|
||||
<span class="mh-type" :class="r.type==='chat_ban'?'chat':'ban'">{{ r.type==='chat_ban'?'Chat-Bann':'Acc-Bann' }}</span>
|
||||
<span class="aa-until">{{ r.ends_at ? timeLeft(r.ends_at) : 'Permanent' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full restriction history with lift/extend -->
|
||||
<div v-if="allR(senderUser).length" class="restrict-hist">
|
||||
<div class="mh-title"><History :size="9" /> Sperr-Historie <span class="mh-count">{{ allR(senderUser).length }}</span></div>
|
||||
<div v-for="r in allR(senderUser)" :key="r.id" class="rh-item" :class="{ active: r.active }">
|
||||
<div class="rh-row">
|
||||
<span class="mh-type" :class="r.type==='chat_ban'?'chat':'ban'">{{ r.type==='chat_ban'?'Chat':'Acc' }}</span>
|
||||
<span class="rh-reason">{{ r.reason || '–' }}</span>
|
||||
<span v-if="r.active" class="mh-live">AKTIV</span>
|
||||
<span class="rh-date">{{ fmt(r.created_at) }}</span>
|
||||
</div>
|
||||
<div v-if="r.ends_at || r.active" class="rh-until">
|
||||
<Clock :size="9" />
|
||||
{{ r.ends_at ? fmt(r.ends_at) : 'Permanent' }}
|
||||
<span v-if="r.active && r.ends_at" class="rh-left">{{ timeLeft(r.ends_at) }}</span>
|
||||
</div>
|
||||
<div v-if="r.active" class="rh-actions">
|
||||
<button class="rha lift" @click="liftRestriction(r.id)">
|
||||
<CheckCircle2 :size="10" /> Aufheben
|
||||
</button>
|
||||
<div class="rha-extend">
|
||||
<input v-model.number="extendHours[r.id]" type="number" min="1" placeholder="Std." class="rha-input" />
|
||||
<button class="rha extend" @click="extendRestriction(r.id)">
|
||||
<Clock :size="10" /> Verlängern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-hist">Keine Sperr-Historie</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ CENTER: Chat timeline ══════════════════════════ -->
|
||||
<div class="col-center">
|
||||
<div class="ct-head">
|
||||
<MessageSquareOff :size="15" class="ct-icon" />
|
||||
<span>Chat-Kontext</span>
|
||||
<span class="ct-sub">{{ (report.context_messages?.length ?? 0) + 1 }} Nachrichten</span>
|
||||
</div>
|
||||
|
||||
<div class="timeline">
|
||||
<div v-if="!report.context_messages?.length && !report.message_text" class="tl-empty">
|
||||
Kein Kontext gespeichert.
|
||||
</div>
|
||||
|
||||
<!-- Context messages -->
|
||||
<div
|
||||
v-for="cm in report.context_messages"
|
||||
:key="cm.id"
|
||||
class="tl-msg"
|
||||
:class="{
|
||||
'by-sender': cm.user?.id === report.sender_id,
|
||||
'by-reporter': cm.user?.id === report.reporter_id,
|
||||
}"
|
||||
>
|
||||
<div class="tl-av" :class="{ 'av-s': cm.user?.id === report.sender_id, 'av-r': cm.user?.id === report.reporter_id }">
|
||||
{{ ini(cm.user?.username) }}
|
||||
</div>
|
||||
<div class="tl-body">
|
||||
<div class="tl-meta">
|
||||
<span class="tl-user">@{{ cm.user?.username || '?' }}</span>
|
||||
<span class="tl-ts">{{ fmt(cm.created_at) }}</span>
|
||||
</div>
|
||||
<div class="tl-text">{{ cm.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ★ Reported message ★ -->
|
||||
<div class="tl-msg reported">
|
||||
<div class="rep-flag"><Flag :size="10" /></div>
|
||||
<div class="tl-av av-s av-rep">{{ ini(report.sender_username) }}</div>
|
||||
<div class="tl-body">
|
||||
<div class="tl-meta">
|
||||
<span class="tl-user rep-user">@{{ report.sender_username || '?' }}</span>
|
||||
<span class="rep-badge"><Flag :size="9" /> Gemeldet</span>
|
||||
<span v-if="rl(report.reason)" class="reason-badge">{{ rl(report.reason) }}</span>
|
||||
<span class="tl-ts">{{ fmt(report.created_at) }}</span>
|
||||
</div>
|
||||
<div class="tl-text rep-text">{{ report.message_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ RIGHT: Punishment ══════════════════════════════ -->
|
||||
<div class="col-right">
|
||||
<div class="pun-head">
|
||||
<Gavel :size="15" class="pun-icon" />
|
||||
<span>Strafe verhängen</span>
|
||||
<span class="ct-sub" v-if="senderUser">@{{ senderUser.username }}</span>
|
||||
<span class="ct-sub warn" v-else>Kein Nutzer verknüpft</span>
|
||||
</div>
|
||||
|
||||
<div class="pun-form" :class="{ locked: !senderUser }">
|
||||
<!-- Type toggle -->
|
||||
<div class="type-row">
|
||||
<button :class="['type-btn', { on: punishType==='chat_ban' }]" @click="punishType='chat_ban'">
|
||||
<MessageSquareOff :size="12" /> Chat-Bann
|
||||
</button>
|
||||
<button :class="['type-btn ban', { on: punishType==='account_ban' }]" @click="punishType='account_ban'">
|
||||
<Ban :size="12" /> Account-Bann
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Templates -->
|
||||
<div class="tpl-block">
|
||||
<div class="tpl-label">Schnell-Vorlagen</div>
|
||||
<div class="tpl-list">
|
||||
<button
|
||||
v-for="t in (punishType==='chat_ban' ? chatTemplates : banTemplates)"
|
||||
:key="t.label"
|
||||
class="tpl-btn"
|
||||
:class="{ on: punishReason===t.reason && punishHours===t.hours, ban: punishType==='account_ban' }"
|
||||
@click="applyTemplate(t.reason, t.hours)"
|
||||
>
|
||||
<span class="tpl-name">{{ t.label }}</span>
|
||||
<span class="tpl-dur">{{ fmtDur(t.hours) }}</span>
|
||||
<span class="tpl-reason">{{ t.reason }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="or-line"><span>oder anpassen</span></div>
|
||||
|
||||
<!-- Custom -->
|
||||
<div class="custom-block">
|
||||
<label class="fl">
|
||||
<span><Clock :size="10" /> Dauer (Stunden)</span>
|
||||
<input v-model.number="punishHours" type="number" min="1" placeholder="leer = permanent" class="fi" />
|
||||
</label>
|
||||
<label class="fl">
|
||||
<span><Shield :size="10" /> Grund</span>
|
||||
<textarea v-model="punishReason" rows="2" placeholder="Begründung..." class="fi ta"></textarea>
|
||||
</label>
|
||||
|
||||
<!-- Preview -->
|
||||
<div v-if="punishReason" class="pun-preview">
|
||||
<span class="pp-type" :class="punishType==='account_ban'?'ban':'chat'">
|
||||
{{ punishType==='chat_ban' ? 'Chat-Bann' : 'Account-Bann' }}
|
||||
</span>
|
||||
<span class="pp-dur">{{ fmtDur(punishHours) }}</span>
|
||||
<span v-if="punishHours" class="pp-until">
|
||||
bis {{ new Date(Date.now() + (punishHours??0)*3600000).toLocaleDateString('de-DE') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="pun-btn"
|
||||
:class="{ ban: punishType==='account_ban' }"
|
||||
:disabled="!punishReason || !senderUser || submitting"
|
||||
@click="submitPunish"
|
||||
>
|
||||
<Loader2 v-if="submitting" :size="13" class="spin" />
|
||||
<Ban v-else-if="punishType==='account_ban'" :size="13" />
|
||||
<MessageSquareOff v-else :size="13" />
|
||||
{{ punishType==='chat_ban' ? 'Chat-Bann verhängen' : 'Account sperren' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /main-grid -->
|
||||
</CasinoAdminLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ─ Page title ─ */
|
||||
.pt { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
||||
.back { display:flex; align-items:center; gap:4px; color:#555; font-size:12px; text-decoration:none; }
|
||||
.back:hover { color:#ccc; }
|
||||
.ptdiv { color:#2a2a2a; }
|
||||
.pt-case { display:flex; align-items:center; gap:3px; font-weight:800; color:#fff; font-size:14px; }
|
||||
.status-chip {
|
||||
font-size:10px; font-weight:800; padding:3px 10px; border-radius:20px;
|
||||
border:1px solid; text-transform:uppercase; letter-spacing:.5px;
|
||||
}
|
||||
|
||||
/* ─ Flash ─ */
|
||||
.flash {
|
||||
display:flex; align-items:center; gap:8px; margin-bottom:16px;
|
||||
background:rgba(34,197,94,0.1); border:1px solid rgba(34,197,94,0.3);
|
||||
color:#22c55e; padding:10px 14px; border-radius:10px; font-size:13px; font-weight:600;
|
||||
}
|
||||
.flash button { margin-left:auto; background:none; border:none; color:inherit; cursor:pointer; display:flex; }
|
||||
.fade-enter-active,.fade-leave-active { transition:opacity .3s; }
|
||||
.fade-enter-from,.fade-leave-to { opacity:0; }
|
||||
|
||||
/* ─ Summary bar ─ */
|
||||
.summary-bar {
|
||||
display:flex; align-items:center; gap:0; flex-wrap:wrap;
|
||||
background:#111113; border:1px solid #1e1e21; border-radius:12px;
|
||||
padding:12px 18px; margin-bottom:18px; gap:16px;
|
||||
}
|
||||
.sb-item { display:flex; flex-direction:column; gap:2px; }
|
||||
.sb-label { font-size:9px; font-weight:700; text-transform:uppercase; letter-spacing:.6px; color:#444; }
|
||||
.sb-val { font-size:12px; font-weight:700; color:#ccc; display:flex; align-items:center; gap:4px; }
|
||||
.sb-val.blue { color:#60a5fa; }
|
||||
.sb-val.pink { color:#f472b6; }
|
||||
.sb-val.mono { font-family:monospace; color:#666; font-size:11px; }
|
||||
.sb-arrow { color:#333; flex-shrink:0; }
|
||||
.sb-sep { width:1px; height:28px; background:#1e1e21; }
|
||||
.sb-status-btns { margin-left:auto; display:flex; gap:6px; }
|
||||
.ssb {
|
||||
display:flex; align-items:center; gap:5px; padding:6px 12px;
|
||||
border-radius:7px; border:1px solid #252528; background:#161618;
|
||||
color:#555; font-size:11px; font-weight:700; cursor:pointer; transition:.15s;
|
||||
}
|
||||
.ssb:hover { border-color:#3a3a3f; color:#aaa; }
|
||||
.ssb.active,.ssb:hover.active { border-color:rgba(34,197,94,0.4); background:rgba(34,197,94,0.08); color:#22c55e; }
|
||||
.ssb.dismiss.active { border-color:rgba(107,114,128,0.4); background:rgba(107,114,128,0.08); color:#6b7280; }
|
||||
|
||||
/* ─ Main grid ─ */
|
||||
.main-grid {
|
||||
display:grid;
|
||||
grid-template-columns: 270px 1fr 290px;
|
||||
gap:16px;
|
||||
align-items:start;
|
||||
}
|
||||
@media(max-width:1300px){ .main-grid { grid-template-columns: 250px 1fr 270px; } }
|
||||
@media(max-width:1000px){ .main-grid { grid-template-columns:1fr; } }
|
||||
|
||||
/* ─ User cards ─ */
|
||||
.col-left { display:flex; flex-direction:column; gap:8px; }
|
||||
.user-card {
|
||||
background:#111113; border:1px solid #1e1e21; border-radius:12px; padding:14px;
|
||||
display:flex; flex-direction:column; gap:9px; position:relative;
|
||||
}
|
||||
.blue-top { border-top:2px solid #3b82f6; }
|
||||
.pink-top { border-top:2px solid #df006a; }
|
||||
|
||||
.uc-label {
|
||||
font-size:9px; font-weight:800; text-transform:uppercase; letter-spacing:.6px;
|
||||
display:flex; align-items:center; gap:3px; padding:2px 8px; border-radius:20px;
|
||||
width:fit-content; border:1px solid;
|
||||
}
|
||||
.uc-label.blue { color:#3b82f6; background:rgba(59,130,246,.08); border-color:rgba(59,130,246,.2); }
|
||||
.uc-label.pink { color:#df006a; background:rgba(223,0,106,.08); border-color:rgba(223,0,106,.2); }
|
||||
|
||||
.uc-row { display:flex; align-items:center; gap:10px; }
|
||||
.uc-av {
|
||||
width:40px; height:40px; border-radius:10px; flex-shrink:0;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
font-size:16px; font-weight:900; overflow:hidden;
|
||||
}
|
||||
.uc-av img { width:100%; height:100%; object-fit:cover; }
|
||||
.uc-av.blue { background:rgba(59,130,246,.15); color:#3b82f6; }
|
||||
.uc-av.pink { background:rgba(223,0,106,.15); color:#df006a; }
|
||||
.uc-info { flex:1; min-width:0; }
|
||||
.uc-name { font-size:13px; font-weight:800; color:#e0e0e0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.uc-email, .uc-since { display:flex; align-items:center; gap:4px; font-size:10px; color:#555; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.uc-open {
|
||||
color:#666; background:#161618; border:1px solid #252528; border-radius:7px;
|
||||
padding:5px; display:flex; cursor:pointer; text-decoration:none; transition:.15s; flex-shrink:0;
|
||||
}
|
||||
.uc-open:hover { color:#ccc; border-color:#3a3a3f; }
|
||||
|
||||
.uc-badges { display:flex; gap:5px; flex-wrap:wrap; }
|
||||
.badge {
|
||||
display:flex; align-items:center; gap:3px;
|
||||
font-size:9px; font-weight:800; padding:2px 6px; border-radius:5px; border:1px solid;
|
||||
text-transform:uppercase;
|
||||
}
|
||||
.badge.role { color:#555; border-color:#252528; background:#161618; }
|
||||
.badge.vip { color:#fcd34d; border-color:rgba(252,211,77,.25); background:rgba(252,211,77,.06); }
|
||||
.badge.banned { color:#ef4444; border-color:rgba(239,68,68,.3); background:rgba(239,68,68,.08); }
|
||||
.badge.cbanned { color:#f97316; border-color:rgba(249,115,22,.3); background:rgba(249,115,22,.08); }
|
||||
|
||||
/* Mini history (reporter) */
|
||||
.mini-hist { border-top:1px solid #1a1a1c; padding-top:8px; }
|
||||
.mh-title { display:flex; align-items:center; gap:4px; font-size:9px; font-weight:700; text-transform:uppercase; color:#444; letter-spacing:.5px; margin-bottom:5px; }
|
||||
.mh-count { margin-left:auto; background:#1a1a1c; border:1px solid #252528; border-radius:8px; padding:0 5px; color:#666; font-size:9px; }
|
||||
.mh-row { display:flex; align-items:center; gap:6px; padding:4px 7px; border-radius:5px; background:#0e0e10; border:1px solid #1a1a1c; margin-bottom:3px; font-size:10px; }
|
||||
.mh-row.active { border-color:rgba(239,68,68,.2); background:rgba(239,68,68,.04); }
|
||||
.mh-type { font-size:9px; font-weight:800; padding:1px 5px; border-radius:3px; }
|
||||
.mh-type.chat { color:#f97316; background:rgba(249,115,22,.1); }
|
||||
.mh-type.ban { color:#ef4444; background:rgba(239,68,68,.1); }
|
||||
.mh-reason { flex:1; color:#666; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.mh-live { color:#ef4444; font-size:12px; }
|
||||
|
||||
/* Active alert */
|
||||
.active-alert {
|
||||
display:flex; align-items:flex-start; gap:8px;
|
||||
background:rgba(239,68,68,.06); border:1px solid rgba(239,68,68,.2);
|
||||
border-radius:8px; padding:8px 10px; color:#ef4444; font-size:12px; font-weight:600;
|
||||
}
|
||||
.aa-title { font-size:11px; font-weight:700; color:#ef4444; margin-bottom:4px; }
|
||||
.aa-list { display:flex; flex-direction:column; gap:3px; }
|
||||
.aa-row { display:flex; align-items:center; justify-content:space-between; gap:8px; }
|
||||
.aa-until { font-size:10px; color:#888; font-weight:500; }
|
||||
|
||||
/* Full restriction history (sender) */
|
||||
.restrict-hist { border-top:1px solid #1a1a1c; padding-top:8px; }
|
||||
.rh-item {
|
||||
border:1px solid #1a1a1c; border-radius:8px; padding:7px 9px;
|
||||
margin-bottom:5px; display:flex; flex-direction:column; gap:4px;
|
||||
background:#0e0e10;
|
||||
}
|
||||
.rh-item.active { border-color:rgba(239,68,68,.25); background:rgba(239,68,68,.03); }
|
||||
.rh-row { display:flex; align-items:center; gap:6px; font-size:11px; }
|
||||
.rh-reason { flex:1; color:#777; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.rh-date { color:#444; font-size:10px; white-space:nowrap; }
|
||||
.rh-until { display:flex; align-items:center; gap:4px; font-size:10px; color:#555; }
|
||||
.rh-left { color:#f97316; font-weight:700; margin-left:4px; }
|
||||
.rh-actions { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
|
||||
.rha {
|
||||
display:flex; align-items:center; gap:3px; font-size:10px; font-weight:700;
|
||||
padding:3px 9px; border-radius:5px; border:1px solid; cursor:pointer; transition:.15s;
|
||||
}
|
||||
.rha.lift { color:#22c55e; border-color:rgba(34,197,94,.3); background:rgba(34,197,94,.07); }
|
||||
.rha.lift:hover { background:rgba(34,197,94,.14); }
|
||||
.rha.extend { color:#f59e0b; border-color:rgba(245,158,11,.3); background:rgba(245,158,11,.07); }
|
||||
.rha.extend:hover { background:rgba(245,158,11,.14); }
|
||||
.rha-extend { display:flex; align-items:center; gap:4px; }
|
||||
.rha-input {
|
||||
width:56px; padding:3px 7px; background:#0d0d0f;
|
||||
border:1px solid #252528; color:#ccc; border-radius:5px; font-size:10px;
|
||||
}
|
||||
.no-hist { font-size:11px; color:#333; font-style:italic; border-top:1px solid #1a1a1c; padding-top:8px; }
|
||||
|
||||
/* Divider */
|
||||
.reported-divider {
|
||||
display:flex; align-items:center; justify-content:center; gap:6px;
|
||||
color:#3a3a3f; font-size:11px; font-weight:700;
|
||||
}
|
||||
.df { color:#df006a; }
|
||||
|
||||
/* ─ Chat timeline ─ */
|
||||
.col-center { min-width:0; }
|
||||
.ct-head {
|
||||
display:flex; align-items:center; gap:7px; margin-bottom:12px;
|
||||
font-size:13px; font-weight:800; color:#e0e0e0;
|
||||
}
|
||||
.ct-icon { color:#df006a; }
|
||||
.ct-sub { font-size:11px; font-weight:400; color:#555; margin-left:2px; }
|
||||
.ct-sub.warn { color:#ef4444; }
|
||||
|
||||
.timeline {
|
||||
background:#0c0c0e; border:1px solid #1a1a1c; border-radius:12px; overflow:hidden;
|
||||
}
|
||||
.tl-empty { padding:40px; text-align:center; color:#3a3a3f; font-size:13px; }
|
||||
|
||||
.tl-msg {
|
||||
display:flex; align-items:flex-start; gap:10px;
|
||||
padding:10px 14px; border-bottom:1px solid #111113; transition:background .1s;
|
||||
}
|
||||
.tl-msg:last-child { border-bottom:none; }
|
||||
.tl-msg:hover { background:rgba(255,255,255,.015); }
|
||||
.tl-msg.by-sender { background:rgba(223,0,106,.03); }
|
||||
.tl-msg.by-reporter { background:rgba(59,130,246,.03); }
|
||||
|
||||
.tl-av {
|
||||
width:28px; height:28px; border-radius:7px; flex-shrink:0;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
font-size:11px; font-weight:800; background:#1a1a1c; color:#555;
|
||||
}
|
||||
.tl-av.av-s { background:rgba(223,0,106,.15); color:#df006a; }
|
||||
.tl-av.av-r { background:rgba(59,130,246,.15); color:#3b82f6; }
|
||||
.tl-av.av-rep { box-shadow:0 0 0 2px rgba(223,0,106,.45); }
|
||||
|
||||
.tl-body { flex:1; min-width:0; }
|
||||
.tl-meta { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:2px; }
|
||||
.tl-user { font-size:11px; font-weight:700; color:#888; }
|
||||
.tl-ts { font-size:10px; color:#3a3a3f; margin-left:auto; }
|
||||
.tl-text { font-size:13px; color:#bbb; line-height:1.45; word-break:break-word; }
|
||||
|
||||
/* Reported msg */
|
||||
.tl-msg.reported {
|
||||
background:rgba(223,0,106,.06) !important;
|
||||
border-left:3px solid #df006a;
|
||||
padding-left:11px;
|
||||
position:relative;
|
||||
}
|
||||
.rep-flag {
|
||||
position:absolute; left:-1px; top:8px;
|
||||
width:18px; height:18px; background:#df006a; color:#fff;
|
||||
border-radius:50%; display:flex; align-items:center; justify-content:center;
|
||||
}
|
||||
.rep-user { color:#f472b6 !important; }
|
||||
.rep-badge {
|
||||
display:flex; align-items:center; gap:3px;
|
||||
font-size:9px; font-weight:800; color:#df006a;
|
||||
background:rgba(223,0,106,.1); border:1px solid rgba(223,0,106,.25);
|
||||
padding:1px 6px; border-radius:4px; text-transform:uppercase;
|
||||
}
|
||||
.reason-badge {
|
||||
font-size:9px; font-weight:700; color:#f59e0b;
|
||||
background:rgba(245,158,11,.1); border:1px solid rgba(245,158,11,.25);
|
||||
padding:1px 6px; border-radius:4px;
|
||||
}
|
||||
.rep-text { color:#fff !important; font-weight:500; }
|
||||
|
||||
/* ─ Punishment panel ─ */
|
||||
.col-right { min-width:0; }
|
||||
.pun-head {
|
||||
display:flex; align-items:center; gap:7px; margin-bottom:14px;
|
||||
font-size:13px; font-weight:800; color:#e0e0e0;
|
||||
}
|
||||
.pun-icon { color:#f59e0b; }
|
||||
|
||||
.pun-form { display:flex; flex-direction:column; gap:14px; }
|
||||
.pun-form.locked { opacity:.45; pointer-events:none; }
|
||||
|
||||
.type-row { display:flex; gap:8px; }
|
||||
.type-btn {
|
||||
flex:1; display:flex; align-items:center; justify-content:center; gap:5px;
|
||||
font-size:12px; font-weight:700; padding:9px; border-radius:9px;
|
||||
border:1px solid #252528; background:#111113; color:#555; cursor:pointer; transition:.15s;
|
||||
}
|
||||
.type-btn.on { border-color:rgba(249,115,22,.4); background:rgba(249,115,22,.1); color:#f97316; }
|
||||
.type-btn.ban.on { border-color:rgba(239,68,68,.4); background:rgba(239,68,68,.1); color:#ef4444; }
|
||||
|
||||
.tpl-block { display:flex; flex-direction:column; gap:6px; }
|
||||
.tpl-label { font-size:10px; font-weight:700; text-transform:uppercase; color:#444; letter-spacing:.5px; }
|
||||
.tpl-list { display:flex; flex-direction:column; gap:4px; }
|
||||
.tpl-btn {
|
||||
display:grid; grid-template-columns:auto 1fr auto;
|
||||
align-items:center; gap:6px; padding:8px 11px;
|
||||
border-radius:8px; border:1px solid #1a1a1c; background:#0e0e10;
|
||||
cursor:pointer; transition:.15s; text-align:left;
|
||||
}
|
||||
.tpl-btn:hover { border-color:#252528; background:#111113; }
|
||||
.tpl-btn.on { border-color:rgba(249,115,22,.35); background:rgba(249,115,22,.07); }
|
||||
.tpl-btn.ban.on { border-color:rgba(239,68,68,.35); background:rgba(239,68,68,.07); }
|
||||
.tpl-name { font-size:12px; font-weight:700; color:#ccc; }
|
||||
.tpl-dur { font-size:10px; font-weight:700; color:#666; background:#161618; border:1px solid #252528; padding:1px 7px; border-radius:5px; justify-self:end; }
|
||||
.tpl-reason { grid-column:1/-1; font-size:10px; color:#555; }
|
||||
|
||||
.or-line {
|
||||
text-align:center; color:#2a2a2a; font-size:10px; font-weight:600;
|
||||
position:relative;
|
||||
}
|
||||
.or-line span { position:relative; z-index:1; background:#0f0f11; padding:0 10px; }
|
||||
.or-line::before { content:''; position:absolute; top:50%; left:0; right:0; height:1px; background:#1a1a1c; }
|
||||
|
||||
.custom-block { display:flex; flex-direction:column; gap:10px; }
|
||||
.fl { display:flex; flex-direction:column; gap:5px; font-size:11px; font-weight:600; color:#555; }
|
||||
.fl span { display:flex; align-items:center; gap:4px; }
|
||||
.fi {
|
||||
background:#0c0c0e; border:1px solid #1e1e21; color:#ccc;
|
||||
padding:8px 11px; border-radius:8px; font-size:13px; width:100%; box-sizing:border-box;
|
||||
}
|
||||
.fi:focus { outline:none; border-color:rgba(223,0,106,.3); }
|
||||
.ta { resize:none; }
|
||||
|
||||
.pun-preview {
|
||||
display:flex; align-items:center; gap:8px; flex-wrap:wrap;
|
||||
background:#0c0c0e; border:1px solid #1e1e21; border-radius:8px; padding:8px 11px; font-size:11px;
|
||||
}
|
||||
.pp-type { font-weight:800; padding:2px 8px; border-radius:5px; }
|
||||
.pp-type.chat { color:#f97316; background:rgba(249,115,22,.1); }
|
||||
.pp-type.ban { color:#ef4444; background:rgba(239,68,68,.1); }
|
||||
.pp-dur { font-weight:700; color:#888; }
|
||||
.pp-until { color:#555; font-size:10px; margin-left:auto; }
|
||||
|
||||
.pun-btn {
|
||||
display:flex; align-items:center; justify-content:center; gap:7px;
|
||||
width:100%; padding:11px; border-radius:10px; border:none; cursor:pointer;
|
||||
font-size:13px; font-weight:800; color:#fff; transition:.2s;
|
||||
background:linear-gradient(135deg,#f97316,#ea580c);
|
||||
}
|
||||
.pun-btn.ban { background:linear-gradient(135deg,#ef4444,#dc2626); }
|
||||
.pun-btn:hover:not(:disabled) { filter:brightness(1.1); }
|
||||
.pun-btn:disabled { opacity:.4; cursor:not-allowed; }
|
||||
.spin { animation:spin .8s linear infinite; }
|
||||
@keyframes spin { to { transform:rotate(360deg); } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user