Files
BetiX/resources/js/pages/Admin/ProfileReportShow.vue
Dolo 0280278978
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
Initialer Laravel Commit für BetiX
2026-04-04 18:01:50 +02:00

1008 lines
49 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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, Image, ExternalLink
} 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 SnapshotComment {
id: number;
content: string;
created_at: string;
user: { id: number; username: string; avatar: string | null };
}
interface ProfileSnapshot {
id: number;
username: string;
avatar: string | null;
banner: string | null;
bio: string | null;
role: string;
vip_level: number;
clan_tag: string | null;
stats: Record<string, any>;
best_wins: any[];
comments: SnapshotComment[];
captured_at: string;
}
interface CurrentProfile {
id: number;
username: string;
avatar: string | null;
banner: string | null;
bio: string | null;
role: string;
vip_level: number;
clan_tag: string | null;
stats: Record<string, any>;
comments: SnapshotComment[];
}
interface Report {
id: number;
reporter_id: number;
profile_id: number;
reason: string;
details: string | null;
snapshot: ProfileSnapshot | null;
screenshot_path: string | null;
status: 'pending' | 'reviewed' | 'dismissed';
admin_note: string | null;
created_at: string;
}
const props = defineProps<{
report: Report;
reporterUser: UserProfile | null;
profileUser: UserProfile | null;
screenshotUrl: string | null;
currentProfile: CurrentProfile | 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/profiles/${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/profiles/${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', harassment: 'Belästigung', inappropriate: 'Unangemessen',
fake: 'Fake', other: '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="`Profil Case #${report.id}`" />
<template #title>
<div class="pt">
<a href="/admin/reports/profiles" class="back"><ArrowLeft :size="14" /> Profil 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">@{{ profileUser?.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-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="Admin ö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>
<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>
<div class="reported-divider"><Flag :size="11" class="df" /> hat gemeldet</div>
<!-- Reported user -->
<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(profileUser)" :src="avUrl(profileUser)!" />
<span v-else>{{ ini(profileUser?.username) }}</span>
</div>
<div class="uc-info">
<div class="uc-name">@{{ profileUser?.username || '' }}</div>
<div class="uc-email"><Mail :size="9" /> {{ profileUser?.email || '' }}</div>
<div v-if="profileUser" class="uc-since"><Calendar :size="9" /> seit {{ new Date(profileUser.created_at).toLocaleDateString('de-DE') }}</div>
</div>
<a v-if="profileUser" :href="`/admin/users/${profileUser.id}`" class="uc-open" title="Admin öffnen"><ChevronRight :size="14" /></a>
</div>
<div class="uc-badges">
<span v-if="profileUser" class="badge role"><Crown :size="8" /> {{ profileUser.role }}</span>
<span v-if="profileUser?.vip_level && profileUser.vip_level > 0" class="badge vip"><Star :size="8" /> VIP {{ profileUser.vip_level }}</span>
<span v-if="profileUser?.is_banned" class="badge banned"><Ban :size="8" /> Gebannt</span>
<span v-if="activeR(profileUser).some(r => r.type==='chat_ban')" class="badge cbanned"><MessageSquareOff :size="8" /> Chat-Bann</span>
</div>
<div v-if="activeR(profileUser).length" class="active-alert">
<AlertTriangle :size="13" />
<div>
<div class="aa-title">{{ activeR(profileUser).length }} aktive Sperre(n)</div>
<div class="aa-list">
<div v-for="r in activeR(profileUser)" :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>
<div v-if="allR(profileUser).length" class="restrict-hist">
<div class="mh-title"><History :size="9" /> Sperr-Historie <span class="mh-count">{{ allR(profileUser).length }}</span></div>
<div v-for="r in allR(profileUser)" :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: Evidence + Current Profile -->
<div class="col-center">
<!-- EVIDENCE BLOCK -->
<div class="block-header evidence">
<Image :size="13" />
<span>Beweise zum Zeitpunkt des Reports</span>
<span class="ct-sub">{{ fmt(report.snapshot?.captured_at ?? report.created_at) }}</span>
</div>
<!-- Screenshot image -->
<div v-if="screenshotUrl" class="screenshot-wrap">
<img :src="screenshotUrl" alt="Profil Screenshot" class="screenshot-img" />
</div>
<div v-else class="no-screenshot-hint">
<Image :size="18" /> Kein Screenshot (älterer Report)
</div>
<!-- Snapshot data -->
<div v-if="report.snapshot" class="profile-snapshot evidence-snap">
<!-- Banner -->
<div class="snap-banner" :style="report.snapshot.banner ? `background-image: url(${report.snapshot.banner})` : ''">
<div class="snap-banner-overlay"></div>
<!-- Avatar overlapping banner -->
<div class="snap-avatar-abs">
<img v-if="report.snapshot.avatar" :src="report.snapshot.avatar" class="snap-av-img" />
<div v-else class="snap-av-fallback">{{ ini(report.snapshot.username) }}</div>
</div>
</div>
<!-- Profile header info -->
<div class="snap-header">
<div class="snap-name-row">
<span class="snap-username">@{{ report.snapshot.username }}</span>
<div class="snap-badges">
<span class="snap-tag role">{{ report.snapshot.role }}</span>
<span class="snap-tag vip" v-if="report.snapshot.vip_level > 0"> VIP {{ report.snapshot.vip_level }}</span>
<span class="snap-tag clan" v-if="report.snapshot.clan_tag">[{{ report.snapshot.clan_tag }}]</span>
</div>
</div>
<p class="snap-bio" v-if="report.snapshot.bio">{{ report.snapshot.bio }}</p>
<p class="snap-bio empty" v-else>Keine Beschreibung</p>
</div>
<!-- Stats row -->
<div class="snap-stats-row" v-if="report.snapshot.stats">
<div class="snap-stat">
<span class="snap-stat-val">{{ report.snapshot.stats.likes_count ?? 0 }}</span>
<span class="snap-stat-label">Likes</span>
</div>
<div class="snap-stat">
<span class="snap-stat-val">{{ report.snapshot.stats.wins ?? 0 }}</span>
<span class="snap-stat-label">Wins</span>
</div>
<div class="snap-stat">
<span class="snap-stat-val">{{ Number(report.snapshot.stats.wagered ?? 0).toFixed(2) }}</span>
<span class="snap-stat-label">Wagered</span>
</div>
<div class="snap-stat">
<span class="snap-stat-val">{{ Number(report.snapshot.stats.biggest_win ?? 0).toFixed(2) }}</span>
<span class="snap-stat-label">Biggest Win</span>
</div>
<div class="snap-stat" v-if="report.snapshot.stats.join_date">
<span class="snap-stat-val">{{ report.snapshot.stats.join_date }}</span>
<span class="snap-stat-label">Joined</span>
</div>
</div>
<!-- Best wins -->
<div v-if="report.snapshot.best_wins?.length" class="snap-section">
<div class="snap-section-title">Top Wins</div>
<div class="snap-wins">
<div v-for="(w, i) in report.snapshot.best_wins" :key="i" class="snap-win-item">
<span class="win-rank">#{{ i + 1 }}</span>
<span class="win-game">{{ w.game_name || w.game || '' }}</span>
<span class="win-amount">{{ Number(w.payout_amount ?? w.payout ?? 0).toFixed(2) }}</span>
</div>
</div>
</div>
<!-- Comments -->
<div class="snap-section">
<div class="snap-section-title">
Kommentare
<span class="snap-count">{{ report.snapshot.comments?.length ?? 0 }}</span>
</div>
<div v-if="report.snapshot.comments?.length" class="snap-comments">
<div v-for="c in report.snapshot.comments" :key="c.id" class="snap-comment">
<div class="sc-avatar">
<img v-if="c.user?.avatar" :src="c.user.avatar" class="sc-av-img" />
<span v-else class="sc-av-fallback">{{ ini(c.user?.username) }}</span>
</div>
<div class="sc-body">
<div class="sc-meta">
<span class="sc-name">@{{ c.user?.username || '?' }}</span>
<span class="sc-ts">{{ fmt(c.created_at) }}</span>
</div>
<div class="sc-text">{{ c.content }}</div>
</div>
</div>
</div>
<div v-else class="snap-empty">Keine Kommentare</div>
</div>
<div class="snap-captured">
Snapshot aufgenommen: {{ fmt(report.snapshot.captured_at) }}
</div>
</div>
<!-- Details text -->
<div v-if="report.details" class="details-block">
<div class="db-label">Beschreibung vom Melder</div>
<div class="db-text">{{ report.details }}</div>
</div>
<!-- CURRENT LIVE PROFILE BLOCK -->
<div class="block-header current">
<ExternalLink :size="13" />
<span>Aktuelles Profil (Live aus DB)</span>
<span class="ct-sub diff-hint">Vergleiche mit den Beweisen oben</span>
<a v-if="profileUser" :href="`/profile/${profileUser.username}`" target="_blank" class="ct-open-link">
<ExternalLink :size="12" /> Öffnen
</a>
</div>
<div v-if="currentProfile" class="profile-snapshot current-snap">
<!-- Banner -->
<div class="snap-banner" :style="currentProfile.banner ? `background-image: url(${currentProfile.banner})` : ''">
<div class="snap-banner-overlay"></div>
<div class="snap-avatar-abs">
<img v-if="currentProfile.avatar" :src="currentProfile.avatar" class="snap-av-img" />
<div v-else class="snap-av-fallback">{{ ini(currentProfile.username) }}</div>
</div>
</div>
<!-- Header -->
<div class="snap-header">
<div class="snap-name-row">
<span class="snap-username">@{{ currentProfile.username }}</span>
<div class="snap-badges">
<span class="snap-tag role">{{ currentProfile.role }}</span>
<span class="snap-tag vip" v-if="currentProfile.vip_level > 0"> VIP {{ currentProfile.vip_level }}</span>
<span class="snap-tag clan" v-if="currentProfile.clan_tag">[{{ currentProfile.clan_tag }}]</span>
</div>
</div>
<p class="snap-bio" v-if="currentProfile.bio">{{ currentProfile.bio }}</p>
<p class="snap-bio empty" v-else>Keine Beschreibung</p>
</div>
<!-- Stats -->
<div class="snap-stats-row">
<div class="snap-stat">
<span class="snap-stat-val">{{ currentProfile.stats?.likes_count ?? 0 }}</span>
<span class="snap-stat-label">Likes</span>
</div>
<div class="snap-stat">
<span class="snap-stat-val">{{ currentProfile.stats?.wins ?? 0 }}</span>
<span class="snap-stat-label">Wins</span>
</div>
<div class="snap-stat">
<span class="snap-stat-val">{{ Number(currentProfile.stats?.wagered ?? 0).toFixed(2) }}</span>
<span class="snap-stat-label">Wagered</span>
</div>
<div class="snap-stat">
<span class="snap-stat-val">{{ Number(currentProfile.stats?.biggest_win ?? 0).toFixed(2) }}</span>
<span class="snap-stat-label">Biggest Win</span>
</div>
</div>
<!-- Comments -->
<div class="snap-section">
<div class="snap-section-title">
Aktuelle Kommentare
<span class="snap-count">{{ currentProfile.comments?.length ?? 0 }}</span>
</div>
<div v-if="currentProfile.comments?.length" class="snap-comments">
<div v-for="c in currentProfile.comments" :key="c.id" class="snap-comment">
<div class="sc-avatar">
<img v-if="c.user?.avatar" :src="c.user.avatar" class="sc-av-img" />
<span v-else class="sc-av-fallback">{{ ini(c.user?.username) }}</span>
</div>
<div class="sc-body">
<div class="sc-meta">
<span class="sc-name">@{{ c.user?.username || '?' }}</span>
<span class="sc-ts">{{ fmt(c.created_at) }}</span>
</div>
<div class="sc-text">{{ c.content }}</div>
</div>
</div>
</div>
<div v-else class="snap-empty">Keine Kommentare</div>
</div>
</div>
<div v-else class="no-screenshot">
<ExternalLink :size="24" />
<span>Profil nicht gefunden (gelöscht?)</span>
</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="profileUser">@{{ profileUser.username }}</span>
<span class="ct-sub warn" v-else>Kein Nutzer verknüpft</span>
</div>
<div class="pun-form" :class="{ locked: !profileUser }">
<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>
<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>
<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>
<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 || !profileUser || 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; 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-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 { 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; }
.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); }
.reported-divider {
display:flex; align-items:center; justify-content:center; gap:6px;
color:#444; font-size:11px; padding:2px 0;
}
.df { color:#df006a; }
/* 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 */
.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; }
.no-hist { font-size:11px; color:#444; font-style:italic; padding-top:4px; }
.rh-actions { display:flex; align-items:center; gap:6px; padding-top:2px; }
.rha {
display:flex; align-items:center; gap:4px; font-size:10px; font-weight:700;
padding:3px 8px; border-radius:5px; cursor:pointer; border:1px solid; background:transparent;
}
.rha.lift { color:#22c55e; border-color:rgba(34,197,94,.3); }
.rha.lift:hover { background:rgba(34,197,94,.1); }
.rha.extend { color:#60a5fa; border-color:rgba(96,165,250,.3); }
.rha.extend:hover { background:rgba(96,165,250,.1); }
.rha-extend { display:flex; align-items:center; gap:4px; margin-left:auto; }
.rha-input {
width:56px; background:#0e0e10; border:1px solid #252528; border-radius:5px;
color:#ccc; font-size:10px; padding:3px 6px; outline:none;
}
/* ─ Center: Evidence + Current ─ */
.col-center { display:flex; flex-direction:column; gap:12px; }
.block-header {
display:flex; align-items:center; gap:8px;
border-radius:10px; padding:10px 14px;
font-size:12px; font-weight:700; color:#ccc;
}
.block-header.evidence {
background:rgba(245,158,11,0.06); border:1px solid rgba(245,158,11,0.2);
color:#f59e0b;
}
.block-header.current {
background:rgba(34,197,94,0.06); border:1px solid rgba(34,197,94,0.2);
color:#22c55e; margin-top:8px;
}
.ct-sub { font-size:10px; color:#666; font-weight:500; }
.diff-hint { color:#555; }
.ct-open-link {
margin-left:auto; display:flex; align-items:center; gap:4px;
color:#888; font-size:11px; text-decoration:none; font-weight:600;
}
.ct-open-link:hover { color:#fff; }
.no-screenshot-hint {
display:flex; align-items:center; gap:8px;
color:#444; font-size:12px; padding:10px 14px;
background:#111113; border:1px solid #1e1e21; border-radius:10px;
}
.screenshot-wrap {
background:#0a0a0c; border:1px solid #1a1a1c; border-radius:12px;
overflow:hidden; position:relative;
}
.screenshot-img {
width:100%; display:block;
border-radius:12px;
}
/* ─ Profile data snapshot ─ */
.profile-snapshot {
background:#111113; border:1px solid #1e1e21; border-radius:12px;
overflow:hidden; display:flex; flex-direction:column;
}
.evidence-snap { border-color:rgba(245,158,11,0.2); }
.current-snap { border-color:rgba(34,197,94,0.2); }
.snap-banner {
height:120px; background:#0a0a0a; background-size:cover; background-position:center;
position:relative; flex-shrink:0;
}
.snap-banner-overlay {
position:absolute; inset:0;
background:linear-gradient(to bottom, transparent 30%, #111113 100%);
}
.snap-avatar-abs {
position:absolute; bottom:-28px; left:18px; z-index:2;
}
.snap-av-img {
width:60px; height:60px; border-radius:12px; object-fit:cover;
border:3px solid #111113;
}
.snap-av-fallback {
width:60px; height:60px; border-radius:12px; background:#1a1a1a;
border:3px solid #111113; display:flex; align-items:center; justify-content:center;
font-size:22px; font-weight:900; color:#444;
}
.snap-header { padding:38px 18px 14px; border-bottom:1px solid #1e1e21; }
.snap-name-row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:6px; }
.snap-username { font-size:16px; font-weight:900; color:#fff; }
.snap-badges { display:flex; gap:5px; flex-wrap:wrap; }
.snap-tag { font-size:10px; font-weight:700; padding:2px 8px; border-radius:20px; border:1px solid; }
.snap-tag.role { color:#888; border-color:#333; background:#1a1a1a; }
.snap-tag.vip { color:#ffd700; border-color:rgba(255,215,0,.3); background:rgba(255,215,0,.05); }
.snap-tag.clan { color:#00f2ff; border-color:rgba(0,242,255,.3); background:rgba(0,242,255,.05); }
.snap-bio { font-size:13px; color:#888; line-height:1.55; margin:0; }
.snap-bio.empty { color:#444; font-style:italic; }
.snap-stats-row {
display:flex; gap:0; border-bottom:1px solid #1e1e21;
}
.snap-stat {
flex:1; display:flex; flex-direction:column; align-items:center;
padding:12px 8px; border-right:1px solid #1e1e21;
}
.snap-stat:last-child { border-right:none; }
.snap-stat-val { font-size:13px; font-weight:800; color:#fff; }
.snap-stat-label { font-size:9px; color:#555; text-transform:uppercase; letter-spacing:.5px; margin-top:2px; }
.snap-section { padding:14px 18px; border-bottom:1px solid #1e1e21; }
.snap-section-title {
display:flex; align-items:center; gap:6px;
font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.6px; color:#444;
margin-bottom:10px;
}
.snap-count {
background:#1a1a1c; border:1px solid #252528; border-radius:8px;
padding:0 6px; color:#666; font-size:9px;
}
.snap-empty { font-size:12px; color:#444; font-style:italic; }
.snap-wins { display:flex; flex-direction:column; gap:5px; }
.snap-win-item {
display:flex; align-items:center; gap:10px; padding:7px 10px;
background:#0e0e10; border:1px solid #1a1a1c; border-radius:8px; font-size:12px;
}
.win-rank { color:#555; font-weight:700; min-width:20px; }
.win-game { flex:1; color:#aaa; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.win-amount { color:#22c55e; font-weight:800; }
.snap-comments { display:flex; flex-direction:column; gap:8px; }
.snap-comment { display:flex; gap:10px; align-items:flex-start; }
.sc-avatar { flex-shrink:0; }
.sc-av-img { width:28px; height:28px; border-radius:8px; object-fit:cover; border:1px solid #222; }
.sc-av-fallback {
width:28px; height:28px; border-radius:8px; background:#1a1a1a; border:1px solid #1e1e21;
display:flex; align-items:center; justify-content:center; font-size:10px; font-weight:900; color:#555;
}
.sc-body { flex:1; min-width:0; }
.sc-meta { display:flex; align-items:center; gap:8px; margin-bottom:3px; }
.sc-name { font-size:11px; font-weight:700; color:#ccc; }
.sc-ts { font-size:10px; color:#444; }
.sc-text { font-size:12px; color:#888; line-height:1.5; word-break:break-word; }
.snap-captured { padding:10px 18px; font-size:10px; color:#333; }
.no-screenshot {
display:flex; flex-direction:column; align-items:center; justify-content:center;
gap:12px; padding:48px 24px; color:#333;
background:#111113; border:1px solid #1e1e21; border-radius:12px;
font-size:13px;
}
.details-block {
background:#111113; border:1px solid #1e1e21; border-radius:10px; padding:12px 14px;
}
.db-label { font-size:10px; color:#555; text-transform:uppercase; letter-spacing:.5px; font-weight:700; margin-bottom:6px; }
.db-text { font-size:13px; color:#999; line-height:1.6; }
/* ─ Right: Punishment ─ */
.col-right { display:flex; flex-direction:column; gap:0; }
.pun-head {
display:flex; align-items:center; gap:8px;
background:#111113; border:1px solid #1e1e21; border-radius:10px 10px 0 0;
border-bottom:none; padding:10px 14px; font-size:12px; font-weight:700; color:#ccc;
}
.pun-icon { color:#f59e0b; flex-shrink:0; }
.ct-sub.warn { color:#ef4444; }
.pun-form {
background:#111113; border:1px solid #1e1e21; border-radius:0 0 12px 12px;
padding:14px; display:flex; flex-direction:column; gap:12px;
}
.pun-form.locked { opacity:.5; pointer-events:none; }
.type-row { display:flex; gap:6px; }
.type-btn {
flex:1; display:flex; align-items:center; justify-content:center; gap:5px;
padding:7px; border-radius:7px; border:1px solid #252528; background:#161618;
color:#555; font-size:11px; font-weight:700; cursor:pointer; transition:.15s;
}
.type-btn.on { border-color:rgba(249,115,22,.4); background:rgba(249,115,22,.08); color:#f97316; }
.type-btn.ban.on { border-color:rgba(239,68,68,.4); background:rgba(239,68,68,.08); color:#ef4444; }
.type-btn:not(.on):hover { border-color:#3a3a3f; color:#aaa; }
.tpl-block { display:flex; flex-direction:column; gap:6px; }
.tpl-label { font-size:9px; font-weight:700; text-transform:uppercase; letter-spacing:.5px; color:#444; }
.tpl-list { display:flex; flex-direction:column; gap:4px; }
.tpl-btn {
display:grid; grid-template-columns:auto 1fr auto;
align-items:center; gap:8px; padding:7px 10px;
border-radius:7px; border:1px solid #1a1a1c; background:#0e0e10;
cursor:pointer; text-align:left; transition:.15s;
}
.tpl-btn:hover { border-color:#2a2a2e; background:#111113; }
.tpl-btn.on { border-color:rgba(249,115,22,.3); background:rgba(249,115,22,.06); }
.tpl-btn.ban.on { border-color:rgba(239,68,68,.3); background:rgba(239,68,68,.06); }
.tpl-name { font-size:11px; font-weight:800; color:#ccc; }
.tpl-dur { font-size:10px; color:#555; text-align:right; }
.tpl-reason { grid-column:1/-1; font-size:10px; color:#555; font-style:italic; margin-top:-4px; }
.or-line {
display:flex; align-items:center; gap:8px; color:#333; font-size:10px;
}
.or-line::before,.or-line::after { content:''; flex:1; height:1px; background:#1a1a1c; }
.custom-block { display:flex; flex-direction:column; gap:8px; }
.fl { display:flex; flex-direction:column; gap:4px; font-size:10px; color:#555; font-weight:600; }
.fi {
background:#0e0e10; border:1px solid #1e1e21; border-radius:7px;
color:#ccc; font-size:12px; padding:7px 10px; outline:none; transition:.15s;
font-family:inherit;
}
.fi:focus { border-color:#3a3a3f; }
.ta { resize:vertical; min-height:56px; }
.pun-preview {
display:flex; align-items:center; gap:8px; padding:8px 10px;
background:#0e0e10; border:1px solid #1a1a1c; border-radius:7px; flex-wrap:wrap;
}
.pp-type { font-size:10px; font-weight:800; padding:2px 7px; border-radius:4px; }
.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-size:11px; font-weight:700; color:#ccc; }
.pp-until { font-size:10px; color:#555; }
.pun-btn {
display:flex; align-items:center; justify-content:center; gap:7px;
width:100%; padding:10px; border-radius:8px;
border:1px solid rgba(249,115,22,.4); background:rgba(249,115,22,.1); color:#f97316;
font-size:12px; font-weight:800; cursor:pointer; transition:.2s;
}
.pun-btn:hover:not(:disabled) { background:rgba(249,115,22,.2); }
.pun-btn.ban { border-color:rgba(239,68,68,.4); background:rgba(239,68,68,.1); color:#ef4444; }
.pun-btn.ban:hover:not(:disabled) { background:rgba(239,68,68,.2); }
.pun-btn:disabled { opacity:.4; cursor:default; }
.spin { animation:spin 1s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
</style>