fix: eliminar polling get_notifications en index.php + debug wamid en broadcast

- index.php: NotificationManager simplificado a stub vacío.
  Los elementos notification-bell / global-notification-toasts no existen en el
  markup → el setInterval cada 7s solo generaba tráfico inútil a get_notifications.php
  Las notificaciones ya se manejan vía SSE en conversations.php

- send_broadcast.php: exponer debug_wamids en respuesta (wamid, wa_status, wa_contact)
  para saber exactamente qué devuelve WhatsApp por cada destinatario.
  Agregar dryRun log del payload completo al error_log para diagnóstico de
  mensajes que WhatsApp acepta (200+wamid) pero no entrega.
This commit is contained in:
Lizandro Guarnizo
2026-03-13 15:26:14 -05:00
parent 6cdc7bd6c4
commit 5211d83e8b
2 changed files with 25 additions and 195 deletions
+22 -1
View File
@@ -90,6 +90,7 @@ try {
$sentCount = 0;
$errorCount = 0;
$errors = [];
$debugWamids = [];
foreach ($users as $user) {
try {
@@ -185,6 +186,19 @@ try {
$processedVariables,
$headerParams
);
// Log payload para diagnóstico
$dryPayload = $whatsappService->sendTemplateMessage(
$user['phone_number'],
$template['template_name'] ?? $template['name'],
$template['language_code'] ?? 'es',
$processedVariables,
$headerParams,
null, null, true // dryRun=true
);
error_log("=== BROADCAST TEMPLATE PAYLOAD (para " . $user['phone_number'] . ") ===");
error_log(json_encode($dryPayload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
error_log("=== FIN PAYLOAD ===");
// Construir contenido para BD (reemplazar variables en body_text)
$messageContent = $template['body_text'] ?? $template['name'];
@@ -218,6 +232,12 @@ try {
if ($isSuccess) {
$sentCount++;
$debugWamids[] = [
'phone' => $user['phone_number'],
'wamid' => $sentMessageId,
'wa_status' => $response['messages'][0]['message_status'] ?? 'accepted',
'wa_contact' => $response['contacts'][0]['wa_id'] ?? null,
];
// Guardar mensaje en BD
$db->insert('conversations', [
@@ -258,7 +278,8 @@ try {
'total_users' => count($users),
'selection_type' => $selectionType,
'message_type' => $messageType,
'errors' => $errorCount > 0 ? $errors : null
'errors' => $errorCount > 0 ? $errors : null,
'debug_wamids' => $debugWamids ?? []
]);
} catch (Exception $e) {
+3 -194
View File
@@ -1576,200 +1576,9 @@ try {
</div>
</div>
<script>
// Global Notification Manager: polls server for unread notifications and shows toasts + native notifications
const NotificationManager = {
interval: 7000,
// IDs de notificaciones ya mostradas para evitar re-sonar/repetir
_seenIds: new Set(),
init() {
this.bell = document.getElementById('notification-bell');
this.countEl = document.getElementById('notification-count');
this.toastsContainer = document.getElementById('global-notification-toasts');
if (this.bell) {
this.bell.addEventListener('click', () => {
// switch to conversations tab
const convLink = document.querySelector('.nav-link[data-tab="conversations"]');
if (convLink) convLink.click();
// Request notification permission on explicit interaction
if ("Notification" in window && Notification.permission === 'default') {
Notification.requestPermission().then(p => console.log('Notification permission:', p));
}
// Al abrir la campana se considera que se atendieron visualmente las notificaciones: limpiar indicador visual
this.bell.classList.remove('has-notifications');
});
}
this.loadNotifications();
this._timer = setInterval(() => this.loadNotifications(), this.interval);
},
async loadNotifications() {
try {
const resp = await fetch('api/get_notifications.php', { credentials: 'same-origin' });
if (resp.status === 401) {
console.warn('Notifications fetch: unauthorized (session may have expired)');
this.showSessionExpiredToast();
return;
}
const json = await resp.json();
console.debug('NotificationManager.loadNotifications response:', json);
if (json && json.success && Array.isArray(json.data)) {
const unreadCount = json.data.length;
if (unreadCount > 0) {
this.countEl.textContent = unreadCount;
this.countEl.style.display = 'inline-block';
this.bell.classList.add('has-notifications');
} else {
this.countEl.style.display = 'none';
this.bell.classList.remove('has-notifications');
}
// Mostrar sólo las nuevas notificaciones (no repetir sonido/visual)
json.data.forEach(n => {
if (!this._seenIds.has(n.id)) {
this._seenIds.add(n.id);
this.showToast(n);
}
});
} else if (json && json.success === false) {
console.warn('Notification API error:', json.error || json);
}
} catch (e) {
console.error('Error loading global notifications', e);
}
},
showSessionExpiredToast() {
const toast = document.createElement('div');
toast.className = 'notification-toast';
toast.style.background = '#fff7f7';
toast.style.border = '1px solid #f5c6cb';
toast.style.padding = '10px 14px';
toast.style.marginTop = '8px';
toast.style.borderRadius = '6px';
toast.style.minWidth = '220px';
const msg = document.createElement('div');
msg.textContent = 'La sesión ha expirado. Por favor, vuelve a iniciar sesión.';
toast.appendChild(msg);
const btn = document.createElement('button');
btn.className = 'btn btn-sm btn-primary';
btn.textContent = 'Ir a login';
btn.onclick = () => { window.location.href = 'login.php'; };
const actions = document.createElement('div');
actions.style.marginTop = '8px';
actions.appendChild(btn);
toast.appendChild(actions);
this.toastsContainer.appendChild(toast);
// auto-eliminar toast de sesión expirada después de 1s
setTimeout(() => { if (toast.parentNode) toast.remove(); }, 1000);
},
showToast(notification) {
const toast = document.createElement('div');
toast.className = 'notification-toast';
toast.style.background = '#fff';
toast.style.padding = '10px 14px';
toast.style.border = '1px solid #ddd';
toast.style.boxShadow = '0 2px 6px rgba(0,0,0,0.12)';
toast.style.marginTop = '8px';
toast.style.borderRadius = '6px';
toast.style.minWidth = '220px';
const msg = document.createElement('div');
msg.textContent = notification.message || 'Nuevo evento';
toast.appendChild(msg);
const actions = document.createElement('div');
actions.style.marginTop = '8px';
actions.style.display = 'flex';
actions.style.gap = '8px';
const openBtn = document.createElement('button');
openBtn.className = 'btn btn-sm btn-primary';
openBtn.textContent = 'Abrir';
openBtn.onclick = async () => {
await fetch('api/mark_notification_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id: notification.id})});
let data = {};
try { data = notification.data ? JSON.parse(notification.data) : {}; } catch(e) {}
const userId = data.user_id || notification.user_id;
if (userId) {
// marcar conversaciones como leídas también
try { await fetch('api/mark_conversation_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({user_id: userId})}); } catch(e) { console.warn('mark_conversation_read failed', e); }
const convLink = document.querySelector('.nav-link[data-tab="conversations"]');
if (convLink) convLink.click();
const tryOpen = () => {
if (window.chatApp && typeof window.chatApp.openConversation === 'function') {
window.chatApp.openConversation(userId, 'Usuario', '');
} else {
setTimeout(tryOpen, 300);
}
};
tryOpen();
}
toast.remove();
};
const dismiss = document.createElement('button');
dismiss.className = 'btn btn-sm btn-secondary';
dismiss.textContent = 'Descartar';
dismiss.onclick = async () => {
await fetch('api/mark_notification_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id: notification.id})});
toast.remove();
};
actions.appendChild(openBtn);
actions.appendChild(dismiss);
toast.appendChild(actions);
this.toastsContainer.appendChild(toast);
// native notification
if ("Notification" in window && Notification.permission === 'granted') {
try {
const data = notification.data ? JSON.parse(notification.data) : {};
const n = new Notification(notification.message || 'Nuevo evento', { body: (data.phone || '') });
n.onclick = function() {
window.focus();
const userId = data.user_id || notification.user_id;
if (userId) {
// marcar conversación leída cuando se abre desde la notification nativa
try { fetch('api/mark_conversation_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({user_id: userId})}); } catch(e) { console.warn('mark_conversation_read failed', e); }
const convLink = document.querySelector('.nav-link[data-tab="conversations"]');
if (convLink) convLink.click();
const tryOpen = () => {
if (window.chatApp && typeof window.chatApp.openConversation === 'function') {
window.chatApp.openConversation(userId, 'Usuario', '');
} else {
setTimeout(tryOpen, 300);
}
};
tryOpen();
}
n.close();
};
// cerrar la notificación nativa automáticamente después de 1s
setTimeout(() => { try { n.close(); } catch(e) {} }, 1000);
} catch (e) { console.warn('native notification failed', e); }
}
// play sound
try {
// usar Web Audio API para reproducir un sonido breve
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sine';
o.frequency.value = 880;
o.connect(g);
g.connect(ctx.destination);
g.gain.value = 0.05;
o.start();
setTimeout(() => { o.stop(); ctx.close(); }, 180);
} catch (e) { console.warn('Notification sound failed', e); }
setTimeout(() => { if (toast.parentNode) toast.remove(); }, 1000);
}
};
// NotificationManager eliminado: las notificaciones se manejan
// vía SSE en conversations.php. Esta página no tiene campana de notificaciones.
const NotificationManager = { init() {} };
// ====== Funciones para Broadcast con Plantillas y Múltiples Usuarios ======