Files
BetiX/resources/js/pages/Admin/Support.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

633 lines
17 KiB
Vue

<script setup lang="ts">
import { Head, router } from '@inertiajs/vue3';
import { ref, computed, onMounted, nextTick } from 'vue';
import UserLayout from '@/layouts/admin/CasinoAdminLayout.vue';
import { usePage } from '@inertiajs/vue3';
const props = defineProps<{ enabled: boolean; threads: any[]; ollama?: { host: string; model: string; healthy: boolean; error?: string | null } }>();
const page = usePage();
const user = computed(() => (page.props.auth as any).user || {});
const enabled = ref<boolean>(props.enabled);
function saveEnable() {
router.post('/admin/support/settings', { enabled: enabled.value }, { preserveScroll: true });
}
function sendReply(tid: string, textRef: any) {
const text = (textRef.value || '').trim();
if (!text) return;
router.post(`/admin/support/threads/${tid}/message`, { text }, { preserveScroll: true, onFinish: () => { textRef.value = ''; } });
}
function formatTime(isoString: string) {
return new Date(isoString).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
const statusMap = {
new: { text: 'NEU', color: '#888', glow: 'rgba(136,136,136,0.4)' },
ai: { text: 'KI AKTIV', color: '#00f2ff', glow: 'rgba(0,242,255,0.4)' },
stopped: { text: 'KI GESTOPPT', color: '#ffd700', glow: 'rgba(255,215,0,0.4)' },
handoff: { text: 'ÜBERGABE', color: '#ff9f43', glow: 'rgba(255,159,67,0.4)' },
agent: { text: 'AGENT', color: '#ff007a', glow: 'rgba(255,0,122,0.4)' },
closed: { text: 'GESCHLOSSEN', color: '#ff3e3e', glow: 'rgba(255,62,62,0.4)' },
};
const getStatus = (status: string) => statusMap[status as keyof typeof statusMap] || statusMap.new;
const getAvatarFallback = (name: string, background: string = 'random', color: string = 'fff') => {
const cleanName = name ? name.replace(/\s/g, '+') : 'User';
return `https://ui-avatars.com/api/?name=${cleanName}&background=${background}&color=${color}`;
};
// Helper to find the correct avatar property
const getUserAvatar = (u: any) => {
if (!u) return null;
// Try multiple common property names
return u.avatar || u.avatar_url || u.profile_photo_url || null;
};
onMounted(() => {
console.log('Support Threads Data:', props.threads); // Debugging output
nextTick(() => { if ((window as any).lucide) (window as any).lucide.createIcons(); });
});
</script>
<template>
<UserLayout>
<Head title="Admin · Support" />
<div class="admin-support-container">
<!-- Header Section -->
<div class="page-header">
<div class="header-content">
<h1 class="page-title">SUPPORT <span class="highlight">DASHBOARD</span></h1>
<p class="page-subtitle">VERWALTUNG & LIVE-CHAT ÜBERSICHT</p>
</div>
<div class="header-actions">
<!-- Global Toggle -->
<div class="status-pill" @click="enabled = !enabled; saveEnable();">
<div class="sp-label">CHAT SYSTEM</div>
<div class="sp-indicator" :class="{ active: enabled }">
<div class="sp-dot"></div>
<span>{{ enabled ? 'AKTIV' : 'INAKTIV' }}</span>
</div>
</div>
</div>
</div>
<!-- Stats / Info Grid -->
<div class="stats-grid">
<!-- Ollama Card -->
<div class="stat-card">
<div class="sc-icon">
<i data-lucide="bot"></i>
</div>
<div class="sc-info">
<div class="sc-label">KI STATUS (OLLAMA)</div>
<div class="sc-value" :class="props.ollama?.healthy ? 'text-cyan' : 'text-red'">
{{ props.ollama?.healthy ? 'ONLINE' : 'OFFLINE' }}
</div>
<div class="sc-sub">{{ props.ollama?.model || 'Kein Modell' }}</div>
</div>
<div class="sc-glow" :class="props.ollama?.healthy ? 'glow-cyan' : 'glow-red'"></div>
</div>
<!-- Active Threads Card -->
<div class="stat-card">
<div class="sc-icon">
<i data-lucide="message-square"></i>
</div>
<div class="sc-info">
<div class="sc-label">AKTIVE CHATS</div>
<div class="sc-value text-magenta">{{ props.threads?.length || 0 }}</div>
<div class="sc-sub">OFFENE ANFRAGEN</div>
</div>
<div class="sc-glow glow-magenta"></div>
</div>
</div>
<!-- Threads List -->
<div class="threads-section">
<h2 class="section-title"><i data-lucide="layers"></i> LAUFENDE UNTERHALTUNGEN</h2>
<div v-if="!props.threads || props.threads.length === 0" class="empty-state">
<div class="es-icon"><i data-lucide="inbox"></i></div>
<h3>Keine aktiven Support-Anfragen</h3>
<p>Alles ruhig! Warte auf neue Nachrichten von Benutzern.</p>
</div>
<div class="threads-grid">
<div v-for="t in props.threads" :key="t.id" class="thread-card" :class="{ 'handoff-mode': t.status === 'handoff' }">
<!-- Thread Header -->
<div class="tc-header">
<div class="user-profile">
<div class="up-avatar">
<img
:src="getUserAvatar(t.user) || getAvatarFallback(t.user?.username)"
:alt="t.user?.username"
:onerror="`this.onerror=null; this.src='${getAvatarFallback(t.user?.username)}'`"
/>
</div>
<div class="up-info">
<div class="up-name">{{ t.user?.username }}</div>
<div class="up-id">ID: {{ t.user?.id }} · {{ t.user?.email }}</div>
</div>
</div>
<div class="tc-status" :style="{ color: getStatus(t.status).color, borderColor: getStatus(t.status).color, boxShadow: `0 0 10px ${getStatus(t.status).glow}` }">
{{ getStatus(t.status).text }}
</div>
</div>
<!-- Topic Badge -->
<div class="tc-topic" v-if="t.topic">
<i data-lucide="hash"></i> {{ t.topic }}
</div>
<!-- Chat Area -->
<div class="tc-chat-window">
<div v-for="m in t.messages" :key="m.id" class="chat-row" :class="m.sender === 'agent' ? 'right' : 'left'">
<!-- Avatar Left (User/AI) -->
<div class="chat-avatar" v-if="m.sender !== 'agent' && m.sender !== 'system'">
<img v-if="m.sender === 'user'"
:src="getUserAvatar(t.user) || getAvatarFallback(t.user?.username)"
:onerror="`this.onerror=null; this.src='${getAvatarFallback(t.user?.username)}'`"
/>
<img v-else-if="m.sender === 'ai'" :src="getAvatarFallback('AI', '00f2ff', '000')" />
</div>
<div class="msg-bubble" :class="m.sender">
<div class="msg-text">{{ m.body }}</div>
<div class="msg-meta">
<span>{{ formatTime(m.at) }}</span>
</div>
</div>
<!-- Avatar Right (Agent) -->
<div class="chat-avatar" v-if="m.sender === 'agent'">
<img
:src="getUserAvatar(user) || getAvatarFallback(user.name, 'ff007a')"
:onerror="`this.onerror=null; this.src='${getAvatarFallback(user.name, 'ff007a')}'`"
/>
</div>
</div>
</div>
<!-- Actions -->
<div class="tc-actions">
<div class="input-group">
<input
type="text"
:ref="el => t._ref = el"
:disabled="t.status==='closed'"
placeholder="Antwort schreiben..."
@keydown.enter="sendReply(t.id, t._ref)"
/>
<button class="btn-send" :disabled="t.status==='closed'" @click="sendReply(t.id, t._ref)">
<i data-lucide="send"></i>
</button>
</div>
<button class="btn-close" @click="router.post(`/admin/support/threads/${t.id}/close`, {}, { preserveScroll: true })" title="Chat schließen">
<i data-lucide="x-circle"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</UserLayout>
</template>
<style scoped>
/* --- Variables & Base --- */
.admin-support-container {
padding: 30px;
max-width: 1400px;
margin: 0 auto;
color: #fff;
font-family: 'Inter', sans-serif;
}
/* --- Header --- */
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 40px;
border-bottom: 1px solid #1f1f1f;
padding-bottom: 20px;
}
.page-title {
font-size: 32px;
font-weight: 900;
letter-spacing: -1px;
margin: 0;
line-height: 1.1;
}
.highlight {
color: #ff007a;
text-shadow: 0 0 20px rgba(255, 0, 122, 0.4);
}
.page-subtitle {
color: #888;
font-size: 12px;
font-weight: 700;
letter-spacing: 1px;
margin-top: 5px;
}
/* --- Status Pill (Toggle) --- */
.status-pill {
background: #0f0f0f;
border: 1px solid #222;
border-radius: 12px;
padding: 6px 8px 6px 16px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
transition: 0.3s;
}
.status-pill:hover {
border-color: #333;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
.sp-label {
font-size: 11px;
font-weight: 800;
color: #666;
}
.sp-indicator {
background: #1a1a1a;
padding: 6px 12px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 800;
color: #555;
transition: 0.3s;
}
.sp-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #444;
transition: 0.3s;
}
.sp-indicator.active {
background: rgba(0, 242, 255, 0.1);
color: #00f2ff;
}
.sp-indicator.active .sp-dot {
background: #00f2ff;
box-shadow: 0 0 8px #00f2ff;
}
/* --- Stats Grid --- */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.stat-card {
background: #0f0f0f;
border: 1px solid #1f1f1f;
border-radius: 16px;
padding: 20px;
display: flex;
align-items: center;
gap: 20px;
position: relative;
overflow: hidden;
transition: 0.3s;
}
.stat-card:hover {
transform: translateY(-3px);
border-color: #333;
}
.sc-icon {
width: 50px;
height: 50px;
background: #141414;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #666;
z-index: 2;
}
.sc-icon i { width: 24px; height: 24px; }
.sc-info { z-index: 2; }
.sc-label { font-size: 10px; font-weight: 800; color: #555; letter-spacing: 1px; margin-bottom: 4px; }
.sc-value { font-size: 20px; font-weight: 900; letter-spacing: -0.5px; }
.sc-sub { font-size: 11px; color: #666; margin-top: 2px; font-family: monospace; }
.text-cyan { color: #00f2ff; text-shadow: 0 0 10px rgba(0,242,255,0.3); }
.text-red { color: #ff3e3e; text-shadow: 0 0 10px rgba(255,62,62,0.3); }
.text-magenta { color: #ff007a; text-shadow: 0 0 10px rgba(255,0,122,0.3); }
.sc-glow {
position: absolute;
top: -50%;
right: -20%;
width: 200px;
height: 200px;
border-radius: 50%;
filter: blur(60px);
opacity: 0.15;
z-index: 1;
}
.glow-cyan { background: #00f2ff; }
.glow-red { background: #ff3e3e; }
.glow-magenta { background: #ff007a; }
/* --- Threads Section --- */
.section-title {
font-size: 14px;
font-weight: 800;
color: #888;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
letter-spacing: 1px;
}
.section-title i { width: 16px; }
.empty-state {
background: #0f0f0f;
border: 1px solid #1f1f1f;
border-radius: 16px;
padding: 60px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.es-icon {
width: 60px;
height: 60px;
background: #141414;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #333;
margin-bottom: 20px;
}
.es-icon i { width: 28px; height: 28px; }
.empty-state h3 { font-size: 18px; font-weight: 700; margin: 0 0 8px 0; color: #fff; }
.empty-state p { font-size: 13px; color: #666; margin: 0; }
.threads-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
/* --- Thread Card --- */
.thread-card {
background: #0f0f0f;
border: 1px solid #1f1f1f;
border-radius: 16px;
overflow: hidden;
display: flex;
flex-direction: column;
transition: 0.3s;
height: 600px; /* Taller for better chat view */
}
.thread-card:hover {
border-color: #333;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.thread-card.handoff-mode {
border-color: #ff9f43;
box-shadow: 0 0 20px rgba(255, 159, 67, 0.15);
}
.tc-header {
padding: 16px;
background: #141414;
border-bottom: 1px solid #1f1f1f;
display: flex;
justify-content: space-between;
align-items: center;
}
.user-profile { display: flex; align-items: center; gap: 12px; }
.up-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
border: 2px solid #222;
}
.up-avatar img { width: 100%; height: 100%; object-fit: cover; }
.up-info { display: flex; flex-direction: column; }
.up-name { font-size: 14px; font-weight: 800; color: #fff; }
.up-id { font-size: 10px; color: #666; font-family: monospace; }
.tc-status {
font-size: 9px;
font-weight: 900;
padding: 4px 8px;
border-radius: 6px;
border: 1px solid;
background: rgba(0,0,0,0.2);
}
.tc-topic {
padding: 8px 16px;
background: #111;
border-bottom: 1px solid #1f1f1f;
font-size: 11px;
color: #888;
display: flex;
align-items: center;
gap: 6px;
}
.tc-topic i { width: 12px; }
/* --- Chat Window --- */
.tc-chat-window {
flex: 1;
background: #0a0a0a;
padding: 16px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.chat-row {
display: flex;
gap: 10px;
align-items: flex-end;
max-width: 85%;
}
.chat-row.left { align-self: flex-start; }
.chat-row.right { align-self: flex-end; flex-direction: row-reverse; }
.chat-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
border: 1px solid #333;
}
.chat-avatar img { width: 100%; height: 100%; object-fit: cover; }
.msg-bubble {
padding: 12px 16px;
border-radius: 16px;
font-size: 13px;
line-height: 1.5;
position: relative;
word-break: break-word;
}
/* User Bubble */
.user {
background: #1a1a1a;
color: #ddd;
border-bottom-left-radius: 2px;
border: 1px solid #222;
}
/* Agent Bubble */
.agent {
background: rgba(255, 0, 122, 0.1);
color: #fff;
border-bottom-right-radius: 2px;
border: 1px solid rgba(255, 0, 122, 0.3);
}
/* AI Bubble */
.ai {
background: rgba(0, 242, 255, 0.05);
color: #00f2ff;
border-bottom-left-radius: 2px;
border: 1px solid rgba(0, 242, 255, 0.2);
}
/* System Bubble */
.system {
background: transparent;
color: #666;
font-size: 11px;
font-style: italic;
padding: 4px;
border: none;
text-align: center;
width: 100%;
}
.chat-row:has(.system) { max-width: 100%; align-self: center; }
.msg-meta {
display: flex;
justify-content: flex-end;
margin-top: 8px;
font-size: 9px;
font-weight: 700;
opacity: 0.5;
gap: 6px;
}
/* --- Actions --- */
.tc-actions {
padding: 12px;
background: #141414;
border-top: 1px solid #1f1f1f;
display: flex;
gap: 10px;
}
.input-group {
flex: 1;
display: flex;
background: #0a0a0a;
border: 1px solid #222;
border-radius: 10px;
padding: 4px;
transition: 0.2s;
}
.input-group:focus-within {
border-color: #ff007a;
box-shadow: 0 0 10px rgba(255,0,122,0.1);
}
.input-group input {
flex: 1;
background: transparent;
border: none;
color: #fff;
padding: 0 10px;
font-size: 13px;
}
.btn-send {
width: 32px;
height: 32px;
background: #ff007a;
border: none;
border-radius: 8px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: 0.2s;
}
.btn-send:hover { background: #d40065; }
.btn-send:disabled { background: #333; cursor: not-allowed; }
.btn-send i { width: 16px; }
.btn-close {
width: 42px;
background: rgba(255, 62, 62, 0.1);
border: 1px solid rgba(255, 62, 62, 0.2);
border-radius: 10px;
color: #ff3e3e;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: 0.2s;
}
.btn-close:hover {
background: rgba(255, 62, 62, 0.2);
border-color: #ff3e3e;
}
.btn-close i { width: 20px; }
/* Scrollbar for chat */
.tc-chat-window::-webkit-scrollbar { width: 4px; }
.tc-chat-window::-webkit-scrollbar-track { background: transparent; }
.tc-chat-window::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
@media (max-width: 768px) {
.page-header { flex-direction: column; align-items: flex-start; gap: 20px; }
.threads-grid { grid-template-columns: 1fr; }
}
</style>