Initialer Laravel Commit für BetiX
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

This commit is contained in:
2026-04-04 18:01:50 +02:00
commit 0280278978
374 changed files with 65210 additions and 0 deletions

View 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>