Files
bot_palmas/admin/DashboardController.php
T

591 lines
26 KiB
PHP

<?php
declare(strict_types=1);
class DashboardController
{
private const PER_PAGE = 25;
// ─── GET /admin/dashboard ─────────────────────────────────────────────────
public static function index(): void
{
SessionAuth::require();
$page = max(1, (int)($_GET['page'] ?? 1));
$filter = trim($_GET['type'] ?? '');
$search = trim($_GET['search'] ?? '');
$date = trim($_GET['date'] ?? date('Y-m-d'));
$user = SessionAuth::user();
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
$date = date('Y-m-d');
}
try {
$db = db();
$stats = self::getStats($db);
[$logs, $total] = self::getLogs($db, $page, $filter, $search, $date);
} catch (\PDOException $e) {
$stats = ['total' => 0, 'msgs' => 0, 'media' => 0, 'statuses' => 0];
$logs = [];
$total = 0;
}
$pages = $total > 0 ? (int)ceil($total / self::PER_PAGE) : 1;
self::render(compact('stats', 'logs', 'total', 'page', 'pages', 'filter', 'search', 'date', 'user'));
}
// ─── GET /admin/live ────────────────────────────────────────────────────
public static function live(): void
{
SessionAuth::require();
$user = SessionAuth::user();
$userName = htmlspecialchars($user['name'] ?? 'Admin', ENT_QUOTES, 'UTF-8');
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
echo <<<HTML
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live &mdash; Palmas360</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',Arial,sans-serif;background:#f0f2f5;color:#1c1e21;font-size:14px}
.topbar{background:linear-gradient(135deg,#0b3d91,#1565c0);color:#fff;padding:13px 24px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;box-shadow:0 2px 8px rgba(0,0,0,.15)}
.topbar h1{font-size:17px;font-weight:700}
.topbar-r{display:flex;align-items:center;gap:10px}
.ubadge{background:rgba(255,255,255,.18);border-radius:20px;padding:3px 12px;font-size:12px}
.btn-nav{background:rgba(255,255,255,.2);color:#fff;border:none;border-radius:8px;padding:6px 14px;font-size:13px;cursor:pointer;text-decoration:none}
.btn-nav:hover{background:rgba(255,255,255,.32)}
.toolbar{padding:14px 24px;display:flex;align-items:center;gap:12px;background:#fff;margin:14px 24px 0;border-radius:10px;box-shadow:0 1px 5px rgba(0,0,0,.06)}
.dot{width:10px;height:10px;border-radius:50%;background:#e74c3c;animation:pulse 1.2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
.dot.paused{background:#aaa;animation:none}
#status{font-size:13px;color:#555}
.btn-pause{background:#0b3d91;color:#fff;border:none;border-radius:7px;padding:6px 16px;font-size:13px;cursor:pointer}
.btn-pause:hover{background:#1565c0}
.counter{margin-left:auto;font-size:12px;color:#888}
.feed{padding:0 24px 24px;margin-top:12px;display:flex;flex-direction:column;gap:6px}
.card{background:#fff;border-radius:10px;padding:13px 16px;box-shadow:0 1px 4px rgba(0,0,0,.07);display:flex;align-items:flex-start;gap:12px;animation:slidein .3s ease}
@keyframes slidein{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
.card-icon{font-size:22px;line-height:1;flex-shrink:0;margin-top:2px}
.card-body{flex:1;min-width:0}
.card-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.card-phone{font-family:monospace;font-size:13px;font-weight:600;color:#0b3d91}
.card-name{font-size:12px;color:#888}
.card-time{margin-left:auto;font-size:11px;color:#bbb;white-space:nowrap}
.card-preview{font-size:13px;color:#444;margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.badge{padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600}
.b-text{background:#e8f4fd;color:#1565c0}
.b-image{background:#fce8fd;color:#7b1fa2}
.b-audio{background:#e8fdf0;color:#1b5e20}
.b-video{background:#fdf3e8;color:#e65100}
.b-document{background:#e8eaf6;color:#283593}
.b-sticker{background:#fff8e1;color:#f57f17}
.b-reaction{background:#fce4ec;color:#880e4f}
.b-location{background:#e0f2f1;color:#004d40}
.b-interactive{background:#ede7f6;color:#4527a0}
.b-button{background:#e3f2fd;color:#0d47a1}
.b-status{background:#f3f4f6;color:#6b7280}
.b-other{background:#f5f5f5;color:#555}
.empty{text-align:center;padding:60px;color:#ccc;font-size:15px}
#modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:999;align-items:center;justify-content:center;padding:16px}
.mc{background:#fff;border-radius:12px;width:100%;max-width:740px;max-height:88vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 8px 40px rgba(0,0,0,.25)}
.mh{background:#0b3d91;color:#fff;padding:14px 20px;display:flex;justify-content:space-between;align-items:center;font-weight:700}
.mx{background:none;border:none;color:#fff;font-size:22px;cursor:pointer}
.mb{padding:16px;overflow-y:auto;flex:1}
pre{background:#1e1e2e;color:#cdd6f4;padding:16px;border-radius:8px;font-size:12px;line-height:1.7;overflow-x:auto;white-space:pre-wrap;word-break:break-all}
</style>
</head>
<body>
<div class="topbar">
<div>
<h1><img src="https://app.palmas360.com/Test/imgLogin/logo.png" alt="Palmas360" style="height:32px;vertical-align:middle;margin-right:8px;filter:brightness(0) invert(1)"> Live Feed</h1>
</div>
<div class="topbar-r">
<a href="/admin/dashboard" class="btn-nav">&#8592; Dashboard</a>
<span class="ubadge">&#128100; {$userName}</span>
<a href="/logout" class="btn-nav">Salir</a>
</div>
</div>
<div class="toolbar">
<span class="dot" id="dot"></span>
<span id="status">Conectado &mdash; actualizando cada 3s</span>
<button class="btn-pause" id="btnPause" onclick="togglePause()">&#9646;&#9646; Pausar</button>
<span class="counter" id="counter">0 eventos</span>
</div>
<div class="feed" id="feed"><p class="empty">Esperando eventos en tiempo real...</p></div>
<div id="modal" onclick="if(event.target===this)closeModal()">
<div class="mc">
<div class="mh"><span>&#128230; Payload JSON</span><button class="mx" onclick="closeModal()">&#x2715;</button></div>
<div class="mb"><pre id="mpre">Cargando...</pre></div>
</div>
</div>
<script>
const ICONS = {text:'&#128172;',image:'&#128247;',audio:'&#127925;',video:'&#127916;',document:'&#128196;',sticker:'&#127774;',reaction:'&#128077;',location:'&#128205;',interactive:'&#128280;',button:'&#128306;',status:'&#128202;'};
const BADGE = t => 'b-'+(ICONS[t]?t:'other');
let lastId = 0;
let paused = false;
let count = 0;
let interval = null;
function togglePause() {
paused = !paused;
document.getElementById('btnPause').innerHTML = paused ? '&#9654; Reanudar' : '&#9646;&#9646; Pausar';
document.getElementById('dot').className = 'dot' + (paused ? ' paused' : '');
document.getElementById('status').textContent = paused ? 'En pausa' : 'Conectado \u2014 actualizando cada 3s';
}
async function poll() {
if (paused) return;
try {
const r = await fetch('/admin/webhook/stream?after=' + lastId);
if (!r.ok) return;
const rows = await r.json();
if (!rows.length) return;
lastId = rows[rows.length - 1].id;
const feed = document.getElementById('feed');
const empty = feed.querySelector('.empty');
if (empty) empty.remove();
rows.forEach(row => {
count++;
const icon = ICONS[row.message_type] || '&#128225;';
const badge = BADGE(row.message_type);
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
<div class="card-icon">\${icon}</div>
<div class="card-body">
<div class="card-top">
<span class="card-phone">\${esc(row.from_number)}</span>
<span class="card-name">\${esc(row.contact_name)}</span>
<span class="badge \${badge}">\${esc(row.message_type)}</span>
<span class="card-time">\${esc(row.received_at.substr(11,8))}</span>
<button onclick="showRaw(\${row.id})" style="margin-left:4px;background:#e8eaf6;color:#0b3d91;border:none;border-radius:5px;padding:2px 8px;font-size:11px;font-weight:600;cursor:pointer">JSON</button>
</div>
<div class="card-preview">\${esc(row.message_preview)}</div>
</div>`;
feed.insertBefore(card, feed.firstChild);
});
document.getElementById('counter').textContent = count + ' evento' + (count !== 1 ? 's' : '');
} catch(e) {}
}
function esc(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
async function showRaw(id) {
document.getElementById('mpre').textContent = 'Cargando...';
document.getElementById('modal').style.display = 'flex';
try {
const r = await fetch('/admin/webhook/raw?id=' + id);
const j = await r.json();
document.getElementById('mpre').textContent = JSON.stringify(j, null, 2);
} catch(e) { document.getElementById('mpre').textContent = 'Error.'; }
}
function closeModal() { document.getElementById('modal').style.display = 'none'; }
document.addEventListener('keydown', e => { if(e.key==='Escape') closeModal(); });
// Arrancar: obtener el último ID y luego empezar a hacer poll
fetch('/admin/webhook/stream?after=0&init=1').then(r=>r.json()).then(rows=>{
if(rows.length) lastId = rows[rows.length-1].id;
}).finally(() => { interval = setInterval(poll, 3000); });
</script>
</body>
</html>
HTML;
exit;
}
// ─── GET /admin/webhook/stream?after=N ───────────────────────────────────
public static function stream(): void
{
SessionAuth::require();
$afterId = max(0, (int)($_GET['after'] ?? 0));
$isInit = isset($_GET['init']);
try {
if ($isInit) {
// En init devolvemos solo el último ID (sin datos) para anclar
$stmt = db()->query('SELECT id FROM webhook_logs ORDER BY id DESC LIMIT 1');
$row = $stmt->fetch();
$afterId = $row ? (int)$row['id'] : 0;
// Devolvemos array vacío — solo queremos anclar lastId en el cliente
header('Content-Type: application/json; charset=utf-8');
echo json_encode([]);
exit;
}
$stmt = db()->prepare("
SELECT id, event_field, from_number, contact_name,
message_type, message_preview, received_at
FROM webhook_logs
WHERE id > ?
ORDER BY id ASC
LIMIT 50
");
$stmt->execute([$afterId]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
header('Content-Type: application/json; charset=utf-8');
echo '[]';
exit;
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode($rows, JSON_UNESCAPED_UNICODE);
exit;
}
// ─── GET /admin/webhook/raw?id=N ─────────────────────────────────────────
public static function getRaw(): void
{
SessionAuth::require();
$id = max(0, (int)($_GET['id'] ?? 0));
if ($id === 0) {
jsonResponse(400, ['error' => 'ID inválido']);
}
try {
$stmt = db()->prepare('SELECT raw_payload FROM webhook_logs WHERE id = ? LIMIT 1');
$stmt->execute([$id]);
$row = $stmt->fetch();
} catch (\PDOException $e) {
jsonResponse(500, ['error' => 'Error de base de datos']);
}
if (!$row) {
jsonResponse(404, ['error' => 'Registro no encontrado']);
}
header('Content-Type: application/json; charset=utf-8');
echo $row['raw_payload'];
exit;
}
// ─── Consultas ────────────────────────────────────────────────────────────
private static function getStats(PDO $db): array
{
$stmt = $db->query("
SELECT
COUNT(*) AS total,
COALESCE(SUM(event_field='messages' AND message_type='text'),0) AS msgs,
COALESCE(SUM(event_field='messages' AND message_type<>'text'),0) AS media,
COALESCE(SUM(event_field='statuses'),0) AS statuses
FROM webhook_logs
WHERE DATE(received_at) = CURDATE()
");
return $stmt->fetch() ?: ['total' => 0, 'msgs' => 0, 'media' => 0, 'statuses' => 0];
}
private static function getLogs(PDO $db, int $page, string $filter, string $search, string $date): array
{
$where = ['DATE(received_at) = ?'];
$params = [$date];
if ($filter !== '') {
$where[] = 'event_field = ?';
$params[] = $filter;
}
if ($search !== '') {
$where[] = '(from_number LIKE ? OR contact_name LIKE ? OR message_preview LIKE ?)';
$like = '%' . $search . '%';
array_push($params, $like, $like, $like);
}
$w = implode(' AND ', $where);
$cnt = $db->prepare("SELECT COUNT(*) FROM webhook_logs WHERE $w");
$cnt->execute($params);
$total = (int)$cnt->fetchColumn();
$limit = self::PER_PAGE;
$offset = ($page - 1) * $limit;
$stmt = $db->prepare("
SELECT id, event_field, from_number, contact_name,
message_type, message_preview, received_at
FROM webhook_logs
WHERE $w
ORDER BY received_at DESC
LIMIT $limit OFFSET $offset
");
$stmt->execute($params);
return [$stmt->fetchAll(), $total];
}
// ─── Helpers de vista ─────────────────────────────────────────────────────
private static function h(mixed $v): string
{
return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
private static function typeLabel(string $field, string $type): string
{
if ($field === 'statuses') {
$cls = match ($type) {
'sent' => 'badge-blue',
'delivered' => 'badge-green',
'read' => 'badge-teal',
'failed' => 'badge-red',
default => 'badge-gray',
};
return '<span class="badge ' . $cls . '">' . self::h($type) . '</span>';
}
return match ($type) {
'text' => '💬 Texto',
'image' => '🖼️ Imagen',
'audio' => '🎵 Audio',
'video' => '🎥 Video',
'document' => '📄 Documento',
'location' => '📍 Ubicación',
'interactive' => '🔘 Interactivo',
'button' => '🔲 Botón',
default => self::h($type),
};
}
// ─── Render ───────────────────────────────────────────────────────────────
private static function render(array $v): void
{
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
// Variables para el heredoc
$userName = self::h($v['user']['name'] ?? 'Admin');
$dateVal = self::h($v['date']);
$maxDate = date('Y-m-d');
$filterVal = self::h($v['filter']);
$searchVal = self::h($v['search']);
$totalStr = number_format((int)$v['total']);
$selMessages = $v['filter'] === 'messages' ? 'selected' : '';
$selStatuses = $v['filter'] === 'statuses' ? 'selected' : '';
$sTotal = (int)$v['stats']['total'];
$sMsgs = (int)$v['stats']['msgs'];
$sMedia = (int)$v['stats']['media'];
$sStatuses = (int)$v['stats']['statuses'];
// Filas de la tabla
$rows = '';
if (empty($v['logs'])) {
$rows = '<tr><td colspan="7" class="empty">Sin eventos para esta fecha.</td></tr>';
} else {
foreach ($v['logs'] as $log) {
$id = (int)$log['id'];
$time = self::h(substr($log['received_at'] ?? '', 11, 8));
$from = self::h($log['from_number'] ?? '—');
$name = self::h($log['contact_name'] ?? '—');
$type = self::typeLabel($log['event_field'] ?? '', $log['message_type'] ?? '');
$preview = self::h(mb_substr($log['message_preview'] ?? '—', 0, 70));
$rows .= "<tr>"
. "<td class='id-col'>#{$id}</td>"
. "<td>{$time}</td>"
. "<td class='phone'>{$from}</td>"
. "<td>{$name}</td>"
. "<td>{$type}</td>"
. "<td class='preview'>{$preview}</td>"
. "<td><button class='btn-json' onclick='showRaw({$id})'>JSON</button></td>"
. "</tr>\n";
}
}
// Paginación
$pager = '';
if ($v['pages'] > 1) {
$pager = '<div class="pager">';
for ($i = 1; $i <= min((int)$v['pages'], 20); $i++) {
$qs = '?page=' . $i
. '&type=' . urlencode($v['filter'])
. '&search=' . urlencode($v['search'])
. '&date=' . urlencode($v['date']);
$active = $i === (int)$v['page'] ? ' active' : '';
$pager .= "<a href='{$qs}' class='pg{$active}'>{$i}</a>";
}
$pager .= '</div>';
}
echo <<<HTML
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin &mdash; Palmas360</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',Arial,sans-serif;background:#f0f2f5;color:#1c1e21;font-size:14px}
/* Topbar */
.topbar{background:linear-gradient(135deg,#0b3d91,#1565c0);color:#fff;padding:13px 24px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;box-shadow:0 2px 8px rgba(0,0,0,.15)}
.topbar h1{font-size:17px;font-weight:700}
.topbar small{font-size:11px;opacity:.8;display:block;margin-top:2px}
.topbar-r{display:flex;align-items:center;gap:10px}
.ubadge{background:rgba(255,255,255,.18);border-radius:20px;padding:3px 12px;font-size:12px}
.btn-out{background:rgba(255,255,255,.2);color:#fff;border:none;border-radius:8px;padding:6px 14px;font-size:13px;cursor:pointer;text-decoration:none}
.btn-out:hover{background:rgba(255,255,255,.32)}
/* Stats */
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;padding:18px 24px 0}
.sc{background:#fff;border-radius:10px;padding:16px;box-shadow:0 1px 5px rgba(0,0,0,.06);border-left:4px solid #1565c0}
.sc .v{font-size:28px;font-weight:700;color:#0b3d91}
.sc .l{font-size:12px;color:#888;margin-top:3px}
.sc.g{border-color:#27ae60}.sc.g .v{color:#1e8449}
.sc.o{border-color:#e67e22}.sc.o .v{color:#ca6f1e}
.sc.s{border-color:#7f8c8d}.sc.s .v{color:#616a6b}
/* Filters */
.fbar{padding:14px 24px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;background:#fff;margin:14px 24px 0;border-radius:10px;box-shadow:0 1px 5px rgba(0,0,0,.06)}
.fbar input,.fbar select{padding:7px 11px;border:1px solid #ddd;border-radius:7px;font-size:13px;outline:none}
.fbar input:focus,.fbar select:focus{border-color:#1565c0}
.bf{background:#0b3d91;color:#fff;border:none;border-radius:7px;padding:7px 16px;font-size:13px;cursor:pointer}
.bf:hover{background:#1565c0}
.rc{margin-left:auto;font-size:12px;color:#888}
/* Table */
.tw{margin:12px 24px 24px;background:#fff;border-radius:10px;box-shadow:0 1px 5px rgba(0,0,0,.06);overflow:hidden}
table{width:100%;border-collapse:collapse}
thead{background:#f8f9fd}
th{padding:11px 14px;text-align:left;font-size:11px;font-weight:700;color:#0b3d91;text-transform:uppercase;letter-spacing:.4px;border-bottom:2px solid #e8eaf6}
td{padding:10px 14px;border-bottom:1px solid #f0f0f0;vertical-align:middle}
tr:hover td{background:#fafbff}
tr:last-child td{border-bottom:none}
.id-col{color:#bbb;font-size:12px;white-space:nowrap}
.phone{font-family:monospace;font-size:13px}
.preview{color:#555;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.empty{text-align:center;padding:48px;color:#aaa;font-size:15px}
/* Badges */
.badge{padding:3px 9px;border-radius:12px;font-size:11px;font-weight:600}
.badge-blue{background:#dbeafe;color:#1d4ed8}
.badge-green{background:#dcfce7;color:#166534}
.badge-teal{background:#ccfbf1;color:#0f766e}
.badge-red{background:#fee2e2;color:#991b1b}
.badge-gray{background:#f3f4f6;color:#6b7280}
/* JSON button */
.btn-json{background:#e8eaf6;color:#0b3d91;border:none;border-radius:5px;padding:4px 10px;font-size:11px;font-weight:600;cursor:pointer}
.btn-json:hover{background:#c5cae9}
/* Paginación */
.pager{display:flex;justify-content:center;padding:14px;gap:5px;flex-wrap:wrap}
.pg{padding:5px 12px;border:1px solid #ddd;border-radius:6px;text-decoration:none;color:#0b3d91;font-size:13px;background:#fff}
.pg.active{background:#0b3d91;color:#fff;border-color:#0b3d91}
.pg:hover:not(.active){background:#f0f2f5}
/* Modal */
.modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:999;align-items:center;justify-content:center;padding:16px}
.mc{background:#fff;border-radius:12px;width:100%;max-width:740px;max-height:88vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 8px 40px rgba(0,0,0,.25)}
.mh{background:#0b3d91;color:#fff;padding:14px 20px;display:flex;justify-content:space-between;align-items:center;font-weight:700}
.mx{background:none;border:none;color:#fff;font-size:22px;cursor:pointer;line-height:1;padding:0 4px}
.mb{padding:16px;overflow-y:auto;flex:1}
pre{background:#1e1e2e;color:#cdd6f4;padding:16px;border-radius:8px;font-size:12px;line-height:1.7;overflow-x:auto;white-space:pre-wrap;word-break:break-all}
@media(max-width:768px){
.stats{grid-template-columns:repeat(2,1fr)}
.fbar,.stats,.tw{padding-left:12px;padding-right:12px;margin-left:0;margin-right:0}
.tw{margin:8px 0 16px}
th:nth-child(4),td:nth-child(4){display:none}
}
</style>
</head>
<body>
<div class="topbar">
<div>
<h1><img src="https://app.palmas360.com/Test/imgLogin/logo.png" alt="Palmas360" style="height:52px;vertical-align:middle;margin-right:12px;filter:brightness(0) invert(1)"> Palmas360 &nbsp;&middot;&nbsp; Admin</h1>
<small>Somos19D</small>
</div>
<div class="topbar-r">
<span class="ubadge">👤 {$userName}</span>
<a href="/logout" class="btn-out">Salir</a>
</div>
</div>
<div class="stats">
<div class="sc"> <div class="v">{$sTotal}</div> <div class="l">📡 Total hoy</div></div>
<div class="sc g"><div class="v">{$sMsgs}</div> <div class="l">💬 Mensajes texto</div></div>
<div class="sc o"><div class="v">{$sMedia}</div> <div class="l">📎 Multimedia</div></div>
<div class="sc s"><div class="v">{$sStatuses}</div> <div class="l">📊 Estados</div></div>
</div>
<div class="fbar">
<form method="GET" action="/admin/dashboard" style="display:flex;gap:8px;flex-wrap:wrap;width:100%;align-items:center">
<input type="date" name="date" value="{$dateVal}" max="{$maxDate}">
<select name="type">
<option value="">Todos los tipos</option>
<option value="messages" {$selMessages}>Mensajes</option>
<option value="statuses" {$selStatuses}>Estados</option>
</select>
<input type="text" name="search" value="{$searchVal}" placeholder="Número, nombre, texto..." style="min-width:185px">
<button type="submit" class="bf">🔍 Filtrar</button>
<span class="rc">{$totalStr} resultado(s)</span>
</form>
</div>
<div class="tw">
<table>
<thead>
<tr>
<th>ID</th>
<th>Hora</th>
<th>Número</th>
<th>Nombre</th>
<th>Tipo</th>
<th>Preview</th>
<th></th>
</tr>
</thead>
<tbody>{$rows}</tbody>
</table>
{$pager}
</div>
<!-- Modal payload -->
<div id="modal" class="modal" onclick="if(event.target===this)closeModal()">
<div class="mc">
<div class="mh">
<span>📦 Payload JSON</span>
<button class="mx" onclick="closeModal()">&#x2715;</button>
</div>
<div class="mb">
<pre id="mpre">Cargando...</pre>
</div>
</div>
</div>
<script>
async function showRaw(id) {
document.getElementById('mpre').textContent = 'Cargando...';
document.getElementById('modal').style.display = 'flex';
try {
const r = await fetch('/admin/webhook/raw?id=' + id);
const j = await r.json();
document.getElementById('mpre').textContent = JSON.stringify(j, null, 2);
} catch (e) {
document.getElementById('mpre').textContent = 'Error al cargar el payload.';
}
}
function closeModal() {
document.getElementById('modal').style.display = 'none';
}
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
(function () {
const titleStyle = 'color: #1e88e5; font-size: 14px; font-weight: 700;';
const subtitleStyle = 'color: #90caf9; font-size: 12px;';
const warningStyle = 'color: #ffb300; font-size: 12px;';
console.log('%cDesarrollado por U-site', titleStyle);
console.log('%cAlgo cool para tu consola: esto es un panel seguro y sólo para gestión.', subtitleStyle);
console.log('%cNo deberías pegar scripts en esta parte del navegador, amigo. Mantén la consola limpia y segura.', warningStyle);
})();
</script>
</body>
</html>
HTML;
exit;
}
}