Initialer Laravel Commit für BetiX
This commit is contained in:
502
resources/js/pages/Admin/CasinoDashboard.vue
Normal file
502
resources/js/pages/Admin/CasinoDashboard.vue
Normal file
@@ -0,0 +1,502 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import { onMounted, nextTick } from 'vue';
|
||||
import CasinoAdminLayout from '@/layouts/admin/CasinoAdminLayout.vue';
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
const props = defineProps<{
|
||||
stats: {
|
||||
total_users: number;
|
||||
total_wagered: number;
|
||||
total_payout: number;
|
||||
house_edge: number;
|
||||
active_bans: number;
|
||||
new_users_24h: number;
|
||||
};
|
||||
chartData: any[];
|
||||
recentBets: any[];
|
||||
recentUsers: any[];
|
||||
}>();
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if ((window as any).lucide) (window as any).lucide.createIcons();
|
||||
|
||||
const ctx = document.getElementById('profitChart') as HTMLCanvasElement;
|
||||
if (ctx) {
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: props.chartData.map(d => d.label),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Profit (USD)',
|
||||
data: props.chartData.map(d => d.profit),
|
||||
borderColor: '#ff007a',
|
||||
backgroundColor: 'rgba(255, 0, 122, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
borderWidth: 3,
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#ff007a'
|
||||
},
|
||||
{
|
||||
label: 'Wagered',
|
||||
data: props.chartData.map(d => d.wagered),
|
||||
borderColor: '#3b82f6',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
tension: 0.4,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: { color: '#a1a1aa', font: { size: 12, weight: 'bold' } }
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
backgroundColor: '#111113',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#a1a1aa',
|
||||
borderColor: '#1f1f22',
|
||||
borderWidth: 1
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
grid: { color: '#1f1f22' },
|
||||
ticks: { color: '#a1a1aa' }
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: '#a1a1aa' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Helper to format large numbers
|
||||
const formatNumber = (num: number) => {
|
||||
return new Intl.NumberFormat('en-US').format(num);
|
||||
};
|
||||
|
||||
// Helper to format currency
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount || 0);
|
||||
};
|
||||
|
||||
// Calculate trend direction (Mock logic - replace with actual DB calculation if needed)
|
||||
const trends = {
|
||||
users: { value: '+5.2%', isPositive: true },
|
||||
wagered: { value: '+12.4%', isPositive: true },
|
||||
bans: { value: '-2', isPositive: true }, // Less bans is positive
|
||||
newUsers: { value: '+15', isPositive: true },
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CasinoAdminLayout>
|
||||
<Head title="Casino Dashboard" />
|
||||
<template #title>
|
||||
Overview
|
||||
</template>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="stats-grid">
|
||||
<!-- Total Users -->
|
||||
<div class="stat-card" style="animation: slideUp 0.3s ease-out;">
|
||||
<div class="stat-header">
|
||||
<div class="stat-label">Total Users</div>
|
||||
<div class="stat-icon bg-blue-500/10 text-blue-400">
|
||||
<i data-lucide="users"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">{{ formatNumber(stats.total_users) }}</div>
|
||||
<div class="stat-trend" :class="trends.users.isPositive ? 'text-green-400' : 'text-red-400'">
|
||||
<i :data-lucide="trends.users.isPositive ? 'trending-up' : 'trending-down'" class="w-4 h-4 inline mr-1"></i>
|
||||
{{ trends.users.value }} from last month
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Wagered -->
|
||||
<div class="stat-card" style="animation: slideUp 0.4s ease-out;">
|
||||
<div class="stat-header">
|
||||
<div class="stat-label">Total Wagered</div>
|
||||
<div class="stat-icon bg-green-500/10 text-green-400">
|
||||
<i data-lucide="dollar-sign"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value text-green-400">{{ formatCurrency(stats.total_wagered) }}</div>
|
||||
<div class="stat-trend text-green-400">
|
||||
<i data-lucide="trending-up" class="w-4 h-4 inline mr-1"></i>
|
||||
{{ trends.wagered.value }} from last month
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- House Edge / GGR -->
|
||||
<div class="stat-card" style="animation: slideUp 0.5s ease-out;">
|
||||
<div class="stat-header">
|
||||
<div class="stat-label">House GGR</div>
|
||||
<div class="stat-icon bg-purple-500/10 text-purple-400">
|
||||
<i data-lucide="trending-up"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value text-purple-400">{{ formatCurrency(stats.house_edge) }}</div>
|
||||
<div class="stat-trend text-gray-400">
|
||||
Total profit (Wager - Payout)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Users 24h -->
|
||||
<div class="stat-card" style="animation: slideUp 0.6s ease-out;">
|
||||
<div class="stat-header">
|
||||
<div class="stat-label">New Users (24h)</div>
|
||||
<div class="stat-icon bg-blue-500/10 text-blue-400">
|
||||
<i data-lucide="user-plus"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">{{ formatNumber(stats.new_users_24h) }}</div>
|
||||
<div class="stat-trend text-green-400">
|
||||
<i data-lucide="arrow-up-right" class="w-4 h-4 inline mr-1"></i>
|
||||
{{ trends.newUsers.value }} vs yesterday
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profit Chart Panel -->
|
||||
<div class="panel chart-panel mb-6" style="animation: fadeIn 0.8s ease-out;">
|
||||
<div class="panel-header">
|
||||
<h3>Performance Overview (7 Days)</h3>
|
||||
<div class="flex gap-4 items-center text-xs text-gray-400">
|
||||
<div class="flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-[#ff007a]"></span> Profit</div>
|
||||
<div class="flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-blue-500"></span> Wagered</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container" style="height: 300px; padding: 20px;">
|
||||
<canvas id="profitChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Sections -->
|
||||
<div class="activity-grid">
|
||||
<!-- Recent Bets Table -->
|
||||
<div class="panel list-panel">
|
||||
<div class="panel-header">
|
||||
<h3>Recent High-Rollers</h3>
|
||||
<button class="btn-ghost">View All</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="activity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Player</th>
|
||||
<th>Game</th>
|
||||
<th class="text-right">Wager</th>
|
||||
<th class="text-right">Payout</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="bet in recentBets" :key="bet.id" class="hover-row">
|
||||
<td class="font-bold flex items-center gap-2">
|
||||
<div class="avatar-small">{{ bet.user?.username?.charAt(0) || '?' }}</div>
|
||||
{{ bet.user?.username || 'Unknown' }}
|
||||
</td>
|
||||
<td class="text-gray-400">{{ bet.game_name }}</td>
|
||||
<td class="text-right font-mono">{{ formatCurrency(bet.wager_amount) }}</td>
|
||||
<td class="text-right font-mono font-bold" :class="bet.payout_amount > bet.wager_amount ? 'text-green-400' : 'text-gray-500'">
|
||||
{{ bet.payout_amount > 0 ? '+' : '' }}{{ formatCurrency(bet.payout_amount) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!recentBets || recentBets.length === 0">
|
||||
<td colspan="4" class="text-center py-8 text-gray-500">No bets recorded yet.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Registrations -->
|
||||
<div class="panel list-panel">
|
||||
<div class="panel-header">
|
||||
<h3>Newest Members</h3>
|
||||
<Link href="/admin/users" class="btn-ghost">Manage Users</Link>
|
||||
</div>
|
||||
<div class="user-list">
|
||||
<div v-for="u in recentUsers" :key="u.id" class="user-item">
|
||||
<div class="avatar">{{ u.username.charAt(0).toUpperCase() }}</div>
|
||||
<div class="u-details">
|
||||
<div class="u-name">{{ u.username }}</div>
|
||||
<div class="u-email">{{ u.email }}</div>
|
||||
</div>
|
||||
<div class="u-time text-xs text-gray-500">
|
||||
{{ new Date(u.created_at).toLocaleDateString() }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!recentUsers || recentUsers.length === 0" class="text-center py-8 text-gray-500">
|
||||
No recent users.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CasinoAdminLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #111113;
|
||||
border: 1px solid #1f1f22;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px -10px rgba(0,0,0,0.3);
|
||||
border-color: #27272a;
|
||||
}
|
||||
|
||||
.stat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-icon i {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Activity Grid */
|
||||
.activity-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.activity-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #111113;
|
||||
border: 1px solid #1f1f22;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #1f1f22;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #3b82f6;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Table */
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.activity-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.activity-table th {
|
||||
padding: 12px 24px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #a1a1aa;
|
||||
text-transform: uppercase;
|
||||
background: #0c0c0e;
|
||||
}
|
||||
|
||||
.activity-table td {
|
||||
padding: 16px 24px;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #1f1f22;
|
||||
color: #e4e4e7;
|
||||
}
|
||||
|
||||
.hover-row:hover td {
|
||||
background: #161618;
|
||||
}
|
||||
|
||||
.activity-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.text-right { text-align: right; }
|
||||
.text-center { text-align: center; }
|
||||
|
||||
/* User List */
|
||||
.user-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #1f1f22;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.user-item:hover {
|
||||
background: #161618;
|
||||
}
|
||||
|
||||
.user-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #27272a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.avatar-small {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #27272a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.u-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.u-name {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.u-email {
|
||||
font-size: 12px;
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.text-green-400 { color: #4ade80; }
|
||||
.text-red-400 { color: #f87171; }
|
||||
.text-blue-400 { color: #60a5fa; }
|
||||
.text-purple-400 { color: #c084fc; }
|
||||
.text-gray-400 { color: #9ca3af; }
|
||||
.text-gray-500 { color: #6b7280; }
|
||||
|
||||
.bg-blue-500\/10 { background-color: rgba(59, 130, 246, 0.1); }
|
||||
.bg-green-500\/10 { background-color: rgba(34, 197, 94, 0.1); }
|
||||
.bg-red-500\/10 { background-color: rgba(239, 68, 68, 0.1); }
|
||||
.bg-purple-500\/10 { background-color: rgba(168, 85, 247, 0.1); }
|
||||
|
||||
.font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user