280 lines
13 KiB
PHP
280 lines
13 KiB
PHP
<?php
|
|
require_once __DIR__ . '/db/config.php';
|
|
|
|
$secret_token = 'lili_admin_2026';
|
|
if (($_GET['token'] ?? '') !== $secret_token) {
|
|
die('Acceso denegado. Se requiere un token válido.');
|
|
}
|
|
|
|
$db = db();
|
|
|
|
// Get active users (last 10 minutes)
|
|
$stmt = $db->query("SELECT COUNT(*) FROM visitor_logs WHERE last_activity > DATE_SUB(NOW(), INTERVAL 10 MINUTE)");
|
|
$active_users = $stmt->fetchColumn();
|
|
|
|
// Get active users with phone numbers
|
|
$stmt = $db->query("SELECT phone_number, country, last_activity FROM visitor_logs WHERE last_activity > DATE_SUB(NOW(), INTERVAL 10 MINUTE) AND phone_number IS NOT NULL AND phone_number != '' ORDER BY last_activity DESC");
|
|
$active_phones = $stmt->fetchAll();
|
|
|
|
// Get country distribution for active users
|
|
$stmt = $db->query("SELECT country, country_code, lat, lon, COUNT(*) as count FROM visitor_logs WHERE last_activity > DATE_SUB(NOW(), INTERVAL 10 MINUTE) GROUP BY country_code");
|
|
$locations = $stmt->fetchAll();
|
|
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Admin Dashboard - Lili Records</title>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<style>
|
|
body { background: #0f172a; color: white; font-family: 'Inter', sans-serif; }
|
|
.card { background: rgba(30, 41, 59, 0.7); border: 1px solid rgba(255,255,255,0.1); color: white; backdrop-filter: blur(10px); }
|
|
#map { height: 500px; border-radius: 15px; margin-top: 20px; }
|
|
.stat-value { font-size: 3rem; font-weight: bold; color: #00e676; }
|
|
.btn-success { background: #00e676; border: none; color: #0f172a; font-weight: bold; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container py-5">
|
|
<div class="row mb-4">
|
|
<div class="col-md-12 text-center">
|
|
<img src="./assets/pasted-20260215-163754-def41f49.png" alt="Logo" style="width: 100px; height: 100px; border-radius: 50%; border: 4px solid #00e676; margin-bottom: 1rem; object-fit: cover; box-shadow: 0 0 25px rgba(0, 230, 118, 0.7);">
|
|
<h1>Panel de Administración Real-Time</h1>
|
|
<p class="text-secondary">Lili Records Radio Statistics</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<div class="card p-4 text-center">
|
|
<h5>Usuarios Conectados</h5>
|
|
<div class="stat-value"><?= $active_users ?></div>
|
|
<p class="text-secondary">En los últimos 10 minutos</p>
|
|
</div>
|
|
|
|
<div class="card p-4 mt-4">
|
|
<h5>Móviles Conectados</h5>
|
|
<ul class="list-group list-group-flush bg-transparent">
|
|
<?php if (empty($active_phones)): ?>
|
|
<li class="list-group-item bg-transparent text-secondary border-secondary">No hay móviles registrados</li>
|
|
<?php else: ?>
|
|
<?php foreach ($active_phones as $phone): ?>
|
|
<li class="list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<span class="fw-bold"><?= htmlspecialchars($phone['phone_number']) ?></span><br>
|
|
<small class="text-secondary"><?= htmlspecialchars($phone['country']) ?></small>
|
|
</div>
|
|
<span class="badge bg-success">Online</span>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card p-4 mt-4">
|
|
<h5>Distribución por País</h5>
|
|
<ul class="list-group list-group-flush bg-transparent">
|
|
<?php foreach ($locations as $loc): ?>
|
|
<li class="list-group-item bg-transparent text-white border-secondary d-flex justify-content-between">
|
|
<span><?= htmlspecialchars($loc['country']) ?></span>
|
|
<span class="badge bg-primary"><?= $loc['count'] ?></span>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<div class="card p-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<h5>Mapa de Conexiones</h5>
|
|
<span class="badge bg-success"><i class="bi bi-broadcast"></i> En vivo</span>
|
|
</div>
|
|
<div id="map"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-4">
|
|
<div class="col-md-6">
|
|
<div class="card p-4 h-100">
|
|
<h5><i class="bi bi-person-heart text-success"></i> Top 5 Artistas Más Pedidos</h5>
|
|
<div id="top-artists" class="mt-3">
|
|
<p class="text-secondary text-center">Cargando estadísticas...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="card p-4 h-100">
|
|
<h5><i class="bi bi-star-fill text-success"></i> Top 5 Canciones Más Pedidas</h5>
|
|
<div id="top-songs" class="mt-3">
|
|
<p class="text-secondary text-center">Cargando estadísticas...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mt-4">
|
|
<div class="col-md-12">
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5><i class="bi bi-music-note-list text-success"></i> Moderación de Peticiones</h5>
|
|
<button class="btn btn-sm btn-outline-success" onclick="fetchRequests()">Actualizar Lista</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-dark table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Artista</th>
|
|
<th>Canción</th>
|
|
<th>Solicitado por</th>
|
|
<th>Fecha</th>
|
|
<th>Estado</th>
|
|
<th>Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="requests-body">
|
|
<tr>
|
|
<td colspan="6" class="text-center text-secondary">Cargando peticiones...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script>
|
|
const map = L.map('map').setView([20, 0], 2);
|
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
|
attribution: '© OpenStreetMap contributors'
|
|
}).addTo(map);
|
|
|
|
const locations = <?= json_encode($locations) ?>;
|
|
locations.forEach(loc => {
|
|
if (loc.lat && loc.lon) {
|
|
L.marker([loc.lat, loc.lon])
|
|
.addTo(map)
|
|
.bindPopup(`<b>${loc.country}</b><br>${loc.count} usuario(s)`);
|
|
}
|
|
});
|
|
|
|
async function fetchRequests() {
|
|
const tbody = document.getElementById('requests-body');
|
|
try {
|
|
const response = await fetch('api/song_requests.php?status=all&limit=50');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
if (data.requests.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-secondary">No hay peticiones registradas</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.requests.map(req => `
|
|
<tr>
|
|
<td>${req.artist}</td>
|
|
<td>${req.song}</td>
|
|
<td><span class="badge bg-secondary">${req.requester}</span></td>
|
|
<td><small>${new Date(req.created_at).toLocaleString()}</small></td>
|
|
<td>
|
|
<span class="badge ${req.status === 'played' ? 'bg-success' : 'bg-warning text-dark'}">
|
|
${req.status === 'played' ? 'Reproducida' : 'Pendiente'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
${req.status === 'pending' ? `
|
|
<button class="btn btn-success" onclick="updateStatus(${req.id}, 'mark_played')" title="Marcar como reproducida">
|
|
<i class="bi bi-play-fill"></i>
|
|
</button>
|
|
` : ''}
|
|
<button class="btn btn-danger" onclick="updateStatus(${req.id}, 'delete')" title="Eliminar">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
} catch (error) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-danger">Error al cargar peticiones</td></tr>';
|
|
}
|
|
}
|
|
|
|
async function fetchStats() {
|
|
const artistsDiv = document.getElementById('top-artists');
|
|
const songsDiv = document.getElementById('top-songs');
|
|
try {
|
|
const response = await fetch('api/song_requests.php?action=stats');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
if (data.top_artists.length === 0) {
|
|
artistsDiv.innerHTML = '<p class="text-secondary text-center">Sin datos aún</p>';
|
|
} else {
|
|
artistsDiv.innerHTML = '<div class="list-group list-group-flush bg-transparent">' +
|
|
data.top_artists.map((item, index) => `
|
|
<div class="list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center">
|
|
<span>${index + 1}. ${item.artist}</span>
|
|
<span class="badge bg-success rounded-pill">${item.count} peticiones</span>
|
|
</div>
|
|
`).join('') + '</div>';
|
|
}
|
|
|
|
if (data.top_songs.length === 0) {
|
|
songsDiv.innerHTML = '<p class="text-secondary text-center">Sin datos aún</p>';
|
|
} else {
|
|
songsDiv.innerHTML = '<div class="list-group list-group-flush bg-transparent">' +
|
|
data.top_songs.map((item, index) => `
|
|
<div class="list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<span>${index + 1}. ${item.song}</span><br>
|
|
<small class="text-secondary">${item.artist}</small>
|
|
</div>
|
|
<span class="badge bg-success rounded-pill">${item.count} peticiones</span>
|
|
</div>
|
|
`).join('') + '</div>';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching stats:', error);
|
|
}
|
|
}
|
|
|
|
async function updateStatus(id, action) {
|
|
if (action === 'delete' && !confirm('¿Estás seguro de eliminar esta petición?')) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('id', id);
|
|
formData.append('action', action);
|
|
|
|
try {
|
|
const response = await fetch('api/song_requests.php', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
fetchRequests();
|
|
} else {
|
|
alert('Error: ' + data.error);
|
|
}
|
|
} catch (error) {
|
|
alert('Error al procesar la solicitud');
|
|
}
|
|
}
|
|
|
|
fetchRequests();
|
|
fetchStats();
|
|
setInterval(() => {
|
|
fetchRequests();
|
|
fetchStats();
|
|
}, 15000);
|
|
</script>
|
|
</body>
|
|
</html>
|