diff --git a/admin/DashboardController.php b/admin/DashboardController.php
index e346bf8..d82aad5 100644
--- a/admin/DashboardController.php
+++ b/admin/DashboardController.php
@@ -51,26 +51,9 @@ class DashboardController
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
+ echo Layout::open('Live Feed', 'live', $user['name'] ?? 'Admin');
echo <<
-
-
-
-
- \${esc(row.from_number)}
- \${esc(row.contact_name)}
- \${esc(row.message_type)}
- \${esc(row.received_at.substr(11,8))}
-
-
-
\${esc(row.message_preview)}
-
`;
- feed.insertBefore(card, feed.firstChild);
- });
- document.getElementById('counter').textContent = count + ' evento' + (count !== 1 ? 's' : '');
- } catch(e) {}
+function addCards(rows) {
+ if (!rows.length) return;
+ const feed = document.getElementById('feed');
+ const empty = feed.querySelector('.empty');
+ if (empty) empty.remove();
+ rows.forEach(row => {
+ count++;
+ const icon = ICONS[row.message_type] || '📡';
+ const card = document.createElement('div');
+ card.className = 'feed-card';
+ card.innerHTML = `
+
+
+ \${esc(row.from_number)}
+ \${esc(row.contact_name)}
+ \${esc(row.message_type)}
+ \${esc(row.received_at.substr(11,8))}
+
+
+
\${esc(row.message_preview)}
+
`;
+ feed.insertBefore(card, feed.firstChild);
+ });
+ document.getElementById('counter').textContent = count + ' evento' + (count !== 1 ? 's' : '');
+ if (document.getElementById('chkSound').checked) {
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
+ const osc = ctx.createOscillator(); const g = ctx.createGain();
+ osc.connect(g); g.connect(ctx.destination);
+ osc.frequency.value = 880; g.gain.setValueAtTime(0.08, ctx.currentTime);
+ g.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.12);
+ osc.start(); osc.stop(ctx.currentTime + 0.12);
+ }
+}
+
+function connect() {
+ es = new EventSource('/admin/webhook/stream/sse');
+ es.onopen = () => {
+ document.getElementById('dot').className = 'dot';
+ document.getElementById('status').textContent = 'Conectado β tiempo real (SSE)';
+ };
+ es.onmessage = e => { if (!paused) try { addCards(JSON.parse(e.data)); } catch(_) {} };
+ es.onerror = () => {
+ document.getElementById('dot').className = 'dot paused';
+ document.getElementById('status').textContent = 'Reconectando...';
+ };
}
function esc(s) {
- return String(s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
+ return String(s||'').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'})[c]);
}
async function showRaw(id) {
@@ -209,17 +176,13 @@ async function showRaw(id) {
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);
+ document.getElementById('mpre').textContent = JSON.stringify(await r.json(), 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); });
+connect();
@@ -227,6 +190,57 @@ HTML;
exit;
}
+ // βββ GET /admin/webhook/stream/sse β Server-Sent Events βββββββββββββββββ
+
+ public static function streamSse(): void
+ {
+ SessionAuth::require();
+
+ header('Content-Type: text/event-stream');
+ header('Cache-Control: no-cache');
+ header('X-Accel-Buffering: no'); // disable nginx buffering
+ header('Connection: keep-alive');
+
+ set_time_limit(0);
+ ignore_user_abort(false);
+
+ $lastId = max(0, (int)($_GET['after'] ?? 0));
+
+ // Anchor to latest ID if no after param
+ if ($lastId === 0) {
+ try {
+ $row = db()->query('SELECT id FROM webhook_logs ORDER BY id DESC LIMIT 1')->fetch();
+ $lastId = $row ? (int)$row['id'] : 0;
+ } catch (\PDOException $e) {}
+ }
+
+ while (!connection_aborted()) {
+ try {
+ $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 20'
+ );
+ $stmt->execute([$lastId]);
+ $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
+
+ if ($rows) {
+ $lastId = (int)end($rows)['id'];
+ echo 'data: ' . json_encode($rows, JSON_UNESCAPED_UNICODE) . "\n\n";
+ } else {
+ echo ": heartbeat\n\n"; // keep connection alive
+ }
+ } catch (\PDOException $e) {
+ echo ": db-error\n\n";
+ }
+
+ ob_flush();
+ flush();
+ sleep(2);
+ }
+ exit;
+ }
+
// βββ GET /admin/webhook/stream?after=N βββββββββββββββββββββββββββββββββββ
public static function stream(): void
@@ -427,12 +441,6 @@ HTML;
$showCompany = empty($v['companyId']);
$companyTh = $showCompany ? '
@@ -670,7 +643,63 @@ async function showRaw(id) {
function closeModal() {
document.getElementById('modal').style.display = 'none';
}
-document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
+document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeModal(); closeDrawer(); } });
+
+// ββ Drawer de pendientes βββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function openDrawer() {
+ document.getElementById('drawer').style.display = 'flex';
+ document.getElementById('drawerOverlay').style.display = 'block';
+ refreshDrawer();
+}
+function closeDrawer() {
+ document.getElementById('drawer').style.display = 'none';
+ document.getElementById('drawerOverlay').style.display = 'none';
+}
+async function refreshDrawer() {
+ const body = document.getElementById('drawerBody');
+ body.innerHTML = '
Cargando...
';
+ try {
+ const r = await fetch('/admin/pending-list');
+ const { items } = await r.json();
+ if (!items || !items.length) {
+ body.innerHTML = '
β
Sin pendientes
';
+ return;
+ }
+ body.innerHTML = items.map(it => `
+
+
+ ${esc(it.phone)}
+ ${esc(it.contact_name || '')}
+ ${(it.created_at||'').substr(11,5)}
+
+
β© ${esc(it.incoming_message || '')}
+
π€ ${esc(getPreview(it.bot_response))}
+
+
+
+
+
`).join('');
+ } catch(e) {
+ body.innerHTML = '
Error al cargar
';
+ }
+}
+function getPreview(resp) {
+ if (!resp) return '';
+ try { const p = JSON.parse(resp); return p.payload || p.text || JSON.stringify(p).substring(0,80); } catch(_) { return String(resp).substring(0,80); }
+}
+async function pendingAction(id, action, btn) {
+ btn.disabled = true;
+ const card = document.getElementById('dp-' + id);
+ try {
+ const r = await fetch('/admin/pending-' + action, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({id}) });
+ const j = await r.json();
+ if (j.status === action + 'd' || j.status) {
+ card.style.opacity = '0';
+ card.style.transition = 'opacity .3s';
+ setTimeout(() => card.remove(), 300);
+ } else { btn.disabled = false; }
+ } catch(e) { btn.disabled = false; alert('Error de conexiΓ³n'); }
+}
(function () {
const titleStyle = 'color: #1e88e5; font-size: 14px; font-weight: 700;';
@@ -710,6 +739,15 @@ HTML;
// βββ GET /admin/pending ββββββββββββββββββββββββββββββββββββββββββββββββ
+ public static function pendingList(): void
+ {
+ SessionAuth::require();
+ header('Content-Type: application/json');
+ $items = PendingApproval::findAll('pending');
+ echo json_encode(['items' => $items], JSON_UNESCAPED_UNICODE);
+ exit;
+ }
+
public static function pending(): void
{
SessionAuth::require();
@@ -719,37 +757,13 @@ HTML;
$items = $companyId ? PendingApproval::findByCompany($companyId, 'pending') : PendingApproval::findAll('pending');
$companies = CompanyRepository::findAll();
+ $sPending = PendingApproval::countPending(null);
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
+ echo Layout::open('Pendientes de AprobaciΓ³n', 'pending', $user['name'] ?? 'Admin', $sPending);
echo <<
-
-
-
-
-
Pendientes — Palmas360
-
-
-
-
-
-
Pendientes de AprobaciΓ³n
-
-
-
👤 {$userName}
-
Salir
-
-
-
+