up
This commit is contained in:
@@ -64,6 +64,9 @@ try {
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
|
||||
<!-- Notification toasts container (global) -->
|
||||
<div id="global-notification-toasts" style="position:fixed; top:12px; right:12px; z-index:9999;"></div>
|
||||
<!-- Header -->
|
||||
<header class="content-header">
|
||||
<div>
|
||||
@@ -77,8 +80,13 @@ try {
|
||||
<div class="status-badge">
|
||||
<span class="status-dot status-online"></span>
|
||||
<span class="status-text">En línea</span>
|
||||
</div>
|
||||
<button class="btn btn-primary me-2" onclick="refreshData()">
|
||||
</div> <!-- Notification bell -->
|
||||
<div class="me-2">
|
||||
<button id="notification-bell" class="btn btn-light position-relative" title="Notificaciones">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span id="notification-count" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="display:none;">0</span>
|
||||
</button>
|
||||
</div> <button class="btn btn-primary me-2" onclick="refreshData()">
|
||||
<i class="fas fa-sync-alt"></i> Actualizar
|
||||
</button>
|
||||
<a href="logout.php" class="btn btn-outline-danger" onclick="return confirm('¿Está seguro que desea cerrar sesión?')">
|
||||
@@ -924,6 +932,152 @@ try {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Global Notification Manager: polls server for unread notifications and shows toasts + native notifications
|
||||
const NotificationManager = {
|
||||
interval: 7000,
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.loadNotifications();
|
||||
this._timer = setInterval(() => this.loadNotifications(), this.interval);
|
||||
},
|
||||
async loadNotifications() {
|
||||
try {
|
||||
const resp = await fetch('api/get_notifications.php');
|
||||
const json = await resp.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';
|
||||
} else {
|
||||
this.countEl.style.display = 'none';
|
||||
}
|
||||
json.data.forEach(n => this.showToast(n));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading global notifications', e);
|
||||
}
|
||||
},
|
||||
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) {
|
||||
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) {
|
||||
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();
|
||||
};
|
||||
} catch (e) { console.warn('native notification failed', e); }
|
||||
}
|
||||
|
||||
// play sound
|
||||
try {
|
||||
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(); }, 18000);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
try { NotificationManager.init(); } catch(e) { console.error('NotificationManager init failed', e); }
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user