454 lines
21 KiB
PHP
454 lines
21 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" />
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<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>
|
|
|
|
<!-- Now Playing Header -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-12">
|
|
<div class="card p-3 border-success" style="background: rgba(0, 230, 118, 0.05);">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div class="d-flex align-items-center">
|
|
<div class="position-relative me-3">
|
|
<img id="admin-track-cover" src="./assets/pasted-20260215-163754-def41f49.png" style="width: 60px; height: 60px; border-radius: 10px; object-fit: cover; border: 2px solid #00e676;">
|
|
<span class="position-absolute bottom-0 end-0 p-1 bg-success border border-light rounded-circle" style="width: 12px; height: 12px;"></span>
|
|
</div>
|
|
<div>
|
|
<span class="text-success fw-bold x-small" style="font-size: 0.7rem; letter-spacing: 1px;">SONANDO AHORA:</span>
|
|
<h4 id="admin-track-title" class="mb-0">Lili Records Radio</h4>
|
|
<p id="admin-track-artist" class="text-secondary mb-0">En vivo desde el estudio</p>
|
|
</div>
|
|
</div>
|
|
<div class="text-end">
|
|
<span id="admin-track-timer" class="badge bg-dark border border-secondary">00:00</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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">
|
|
<h5><i class="bi bi-graph-up-arrow text-success"></i> Volumen de Peticiones (Últimas 24h)</h5>
|
|
<div style="height: 250px;">
|
|
<canvas id="volumeChart"></canvas>
|
|
</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-images text-success"></i> Moderación de Galería y Chat</h5>
|
|
<button class="btn btn-sm btn-outline-success" onclick="fetchChatImages()">Cargar Imágenes Recientes</button>
|
|
</div>
|
|
<div class="row" id="chat-images-container">
|
|
<div class="col-12 text-center text-secondary py-4">
|
|
Cargando imágenes del chat...
|
|
</div>
|
|
</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 src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.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);
|
|
|
|
let heatmapLayer;
|
|
let markersGroup = L.layerGroup().addTo(map);
|
|
|
|
function updateMap(locations) {
|
|
markersGroup.clearLayers();
|
|
const heatData = [];
|
|
|
|
locations.forEach(loc => {
|
|
if (loc.lat && loc.lon) {
|
|
// Add marker
|
|
L.marker([loc.lat, loc.lon])
|
|
.addTo(markersGroup)
|
|
.bindPopup(`<b>${loc.country}</b><br>${loc.count} usuario(s)`);
|
|
|
|
// Add to heatmap data (lat, lon, intensity)
|
|
// We use the count as intensity, but capped/scaled for better visualization
|
|
heatData.push([loc.lat, loc.lon, Math.min(loc.count * 0.5, 1)]);
|
|
}
|
|
});
|
|
|
|
if (heatmapLayer) {
|
|
map.removeLayer(heatmapLayer);
|
|
}
|
|
|
|
heatmapLayer = L.heatLayer(heatData, {
|
|
radius: 25,
|
|
blur: 15,
|
|
maxZoom: 10,
|
|
gradient: {0.4: 'blue', 0.6: 'cyan', 0.7: 'lime', 0.8: 'yellow', 1: 'red'}
|
|
}).addTo(map);
|
|
}
|
|
|
|
// Initial map data
|
|
updateMap(<?= json_encode($locations) ?>);
|
|
|
|
let volumeChart;
|
|
function initChart() {
|
|
const ctx = document.getElementById('volumeChart').getContext('2d');
|
|
volumeChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: Array.from({length: 24}, (_, i) => `${i}:00`),
|
|
datasets: [{
|
|
label: 'Peticiones',
|
|
data: new Array(24).fill(0),
|
|
borderColor: '#00e676',
|
|
backgroundColor: 'rgba(0, 230, 118, 0.1)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#94a3b8' } },
|
|
x: { grid: { display: false }, ticks: { color: '#94a3b8' } }
|
|
},
|
|
plugins: {
|
|
legend: { display: false }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
initChart();
|
|
|
|
async function fetchRequests() {
|
|
// ... (keeping existing logic)
|
|
}
|
|
|
|
async function fetchChatImages() {
|
|
const container = document.getElementById('chat-images-container');
|
|
try {
|
|
const response = await fetch('api/chat.php');
|
|
const messages = await response.json();
|
|
|
|
const images = messages.filter(m => m.type === 'image');
|
|
|
|
if (images.length === 0) {
|
|
container.innerHTML = '<div class="col-12 text-center text-secondary py-4">No hay imágenes recientes en el chat.</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = images.map(img => `
|
|
<div class="col-md-3 mb-4">
|
|
<div class="card h-100 p-2" style="background: rgba(0,0,0,0.3);">
|
|
<img src="${img.message}" class="card-img-top rounded" style="height: 150px; object-fit: cover;">
|
|
<div class="card-body p-2">
|
|
<p class="small mb-1">Enviado por: <b>${img.username}</b></p>
|
|
<div class="input-group input-group-sm mb-2">
|
|
<input type="text" class="form-control bg-dark text-white border-secondary" placeholder="Leyenda..." id="caption-${img.id}">
|
|
</div>
|
|
<button class="btn btn-sm btn-success w-100" onclick="promoteToGallery(${img.id})">
|
|
<i class="bi bi-star-fill"></i> Publicar en Galería
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (error) {
|
|
container.innerHTML = '<div class="col-12 text-center text-danger py-4">Error al cargar imágenes.</div>';
|
|
}
|
|
}
|
|
|
|
async function promoteToGallery(messageId) {
|
|
const caption = document.getElementById(`caption-${messageId}`).value;
|
|
try {
|
|
const response = await fetch('api/gallery.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'promote',
|
|
message_id: messageId,
|
|
caption: caption
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
alert('Imagen publicada en la galería con éxito.');
|
|
fetchChatImages();
|
|
} else {
|
|
alert('Error: ' + data.error);
|
|
}
|
|
} catch (error) {
|
|
alert('Error de conexión.');
|
|
}
|
|
}
|
|
|
|
fetchChatImages();
|
|
|
|
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>';
|
|
}
|
|
|
|
if (data.volume_stats && volumeChart) {
|
|
const hourlyData = new Array(24).fill(0);
|
|
data.volume_stats.forEach(stat => {
|
|
hourlyData[stat.hour] = stat.count;
|
|
});
|
|
volumeChart.data.datasets[0].data = hourlyData;
|
|
volumeChart.update();
|
|
}
|
|
|
|
if (data.locations) {
|
|
updateMap(data.locations);
|
|
}
|
|
}
|
|
} 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');
|
|
}
|
|
}
|
|
|
|
async function updateNowPlaying() {
|
|
try {
|
|
const response = await fetch('https://www.radioking.com/widgets/api/v1/radio/828046/track/current');
|
|
const data = await response.json();
|
|
if (data && data.title) {
|
|
document.getElementById('admin-track-title').textContent = data.title;
|
|
document.getElementById('admin-track-artist').textContent = data.artist || 'Lili Records';
|
|
if (data.cover) {
|
|
document.getElementById('admin-track-cover').src = data.cover;
|
|
}
|
|
|
|
if (data.started_at && data.end_at) {
|
|
const start = new Date(data.started_at).getTime();
|
|
const end = new Date(data.end_at).getTime();
|
|
const duration = end - start;
|
|
const now = new Date().getTime();
|
|
const elapsed = Math.max(0, now - start);
|
|
const m = Math.floor(elapsed / 60000);
|
|
const s = Math.floor((elapsed % 60000) / 1000);
|
|
document.getElementById('admin-track-timer').textContent = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching now playing:', error);
|
|
}
|
|
}
|
|
|
|
fetchRequests();
|
|
fetchStats();
|
|
updateNowPlaying();
|
|
setInterval(() => {
|
|
fetchRequests();
|
|
fetchStats();
|
|
updateNowPlaying();
|
|
}, 15000);
|
|
</script>
|
|
</body>
|
|
</html>
|