Messenger-artiges Benachrichtigungssystem für Dolibarr: - Schwebendes FAB (Floating Action Button) unten links - Ausklappbares Panel mit allen Benachrichtigungen - Historie der gelesenen Nachrichten - Direktes Abhaken per Checkbox - Click-to-Navigate für Aktionen - Pulsierender Button bei dringenden Nachrichten - Draggable Panel-Header - Automatische Erkennung hängender Cron-Jobs - API für andere Module: GlobalNotify::error(), ::warning(), etc. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
330 lines
7.8 KiB
JavaScript
330 lines
7.8 KiB
JavaScript
/**
|
|
* GlobalNotify JavaScript
|
|
* Floating messenger-style notification widget
|
|
*/
|
|
|
|
var GlobalNotify = {
|
|
isOpen: false,
|
|
isDragging: false,
|
|
dragOffset: { x: 0, y: 0 },
|
|
|
|
/**
|
|
* Toggle panel visibility
|
|
*/
|
|
toggle: function() {
|
|
var panel = document.getElementById('globalnotify-panel');
|
|
if (!panel) return;
|
|
|
|
if (this.isOpen) {
|
|
this.close();
|
|
} else {
|
|
this.open();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Open panel
|
|
*/
|
|
open: function() {
|
|
var panel = document.getElementById('globalnotify-panel');
|
|
if (!panel) return;
|
|
|
|
panel.style.display = 'flex';
|
|
this.isOpen = true;
|
|
|
|
// Close when clicking outside
|
|
setTimeout(function() {
|
|
document.addEventListener('click', GlobalNotify.handleOutsideClick);
|
|
}, 10);
|
|
},
|
|
|
|
/**
|
|
* Close panel
|
|
*/
|
|
close: function() {
|
|
var panel = document.getElementById('globalnotify-panel');
|
|
if (!panel) return;
|
|
|
|
panel.style.display = 'none';
|
|
this.isOpen = false;
|
|
document.removeEventListener('click', GlobalNotify.handleOutsideClick);
|
|
},
|
|
|
|
/**
|
|
* Handle click outside
|
|
*/
|
|
handleOutsideClick: function(event) {
|
|
var widget = document.getElementById('globalnotify-widget');
|
|
if (widget && !widget.contains(event.target)) {
|
|
GlobalNotify.close();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Toggle history section
|
|
*/
|
|
toggleHistory: function() {
|
|
var toggle = document.querySelector('.globalnotify-history-toggle');
|
|
var history = document.getElementById('globalnotify-history');
|
|
|
|
if (!toggle || !history) return;
|
|
|
|
if (history.classList.contains('open')) {
|
|
history.classList.remove('open');
|
|
toggle.classList.remove('open');
|
|
} else {
|
|
history.classList.add('open');
|
|
toggle.classList.add('open');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Toggle item (mark as read/unread)
|
|
*/
|
|
toggleItem: function(notificationId, event) {
|
|
if (event) {
|
|
event.stopPropagation();
|
|
}
|
|
|
|
var item = document.querySelector('.globalnotify-item[data-id="' + notificationId + '"]');
|
|
if (!item) return;
|
|
|
|
// Visual feedback
|
|
item.style.opacity = '0.5';
|
|
|
|
// AJAX call to toggle read status
|
|
this.ajaxCall('dismiss', { id: notificationId }, function(data) {
|
|
if (data.success) {
|
|
// Move item to history or remove
|
|
item.style.transition = 'opacity 0.3s, transform 0.3s';
|
|
item.style.opacity = '0';
|
|
item.style.transform = 'translateX(-100%)';
|
|
|
|
setTimeout(function() {
|
|
item.remove();
|
|
GlobalNotify.updateBadge();
|
|
GlobalNotify.updateCounts();
|
|
}, 300);
|
|
} else {
|
|
item.style.opacity = '1';
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Go to action URL and mark as read
|
|
*/
|
|
goToAction: function(notificationId, url) {
|
|
// Mark as read first
|
|
this.ajaxCall('dismiss', { id: notificationId }, function() {
|
|
// Navigate to URL
|
|
window.location.href = url;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Mark all as read
|
|
*/
|
|
markAllRead: function() {
|
|
var section = document.querySelector('.globalnotify-section');
|
|
var items = section ? section.querySelectorAll('.globalnotify-item') : [];
|
|
|
|
// Visual feedback
|
|
items.forEach(function(item) {
|
|
item.style.opacity = '0.5';
|
|
});
|
|
|
|
this.ajaxCall('markallread', {}, function(data) {
|
|
if (data.success) {
|
|
// Clear the section
|
|
if (section) {
|
|
section.innerHTML = '<div class="globalnotify-empty"><span class="fa fa-check-circle"></span><br>Keine Benachrichtigungen</div>';
|
|
}
|
|
GlobalNotify.updateBadge();
|
|
GlobalNotify.updateCounts();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Update badge on FAB
|
|
*/
|
|
updateBadge: function() {
|
|
var badge = document.querySelector('.globalnotify-fab-badge');
|
|
var items = document.querySelectorAll('.globalnotify-section .globalnotify-item');
|
|
var count = items.length;
|
|
|
|
if (count > 0) {
|
|
if (badge) {
|
|
badge.textContent = count;
|
|
} else {
|
|
// Create badge
|
|
var fab = document.getElementById('globalnotify-fab');
|
|
if (fab) {
|
|
badge = document.createElement('span');
|
|
badge.className = 'globalnotify-fab-badge';
|
|
badge.textContent = count;
|
|
fab.appendChild(badge);
|
|
}
|
|
}
|
|
} else if (badge) {
|
|
badge.remove();
|
|
}
|
|
|
|
// Update urgent state
|
|
var fab = document.getElementById('globalnotify-fab');
|
|
if (fab) {
|
|
var hasUrgent = document.querySelector('.globalnotify-item-error, .globalnotify-item-action');
|
|
if (hasUrgent && count > 0) {
|
|
fab.classList.add('globalnotify-fab-urgent');
|
|
} else {
|
|
fab.classList.remove('globalnotify-fab-urgent');
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update header counts
|
|
*/
|
|
updateCounts: function() {
|
|
var title = document.querySelector('.globalnotify-panel-title');
|
|
if (!title) return;
|
|
|
|
var unreadItems = document.querySelectorAll('.globalnotify-section .globalnotify-item');
|
|
var historyItems = document.querySelectorAll('.globalnotify-history .globalnotify-item');
|
|
var unreadCount = unreadItems.length;
|
|
var totalCount = unreadCount + historyItems.length;
|
|
|
|
title.textContent = 'Benachrichtigungen (' + unreadCount + '/' + totalCount + ')';
|
|
|
|
// Hide/show mark all link
|
|
var markAll = document.querySelector('.globalnotify-panel-actions .globalnotify-action-link');
|
|
if (markAll) {
|
|
markAll.style.display = unreadCount > 0 ? '' : 'none';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* AJAX helper
|
|
*/
|
|
ajaxCall: function(action, params, callback) {
|
|
var url = (typeof DOL_URL_ROOT !== 'undefined' ? DOL_URL_ROOT : '') + '/custom/globalnotify/ajax/action.php';
|
|
var body = 'action=' + encodeURIComponent(action);
|
|
|
|
for (var key in params) {
|
|
body += '&' + key + '=' + encodeURIComponent(params[key]);
|
|
}
|
|
|
|
if (typeof TOKEN !== 'undefined') {
|
|
body += '&token=' + TOKEN;
|
|
}
|
|
|
|
fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: body
|
|
})
|
|
.then(function(response) { return response.json(); })
|
|
.then(function(data) {
|
|
if (callback) callback(data);
|
|
})
|
|
.catch(function(error) {
|
|
console.error('GlobalNotify error:', error);
|
|
if (callback) callback({ success: false, error: error });
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Initialize dragging
|
|
*/
|
|
initDrag: function() {
|
|
var widget = document.getElementById('globalnotify-widget');
|
|
var handle = document.getElementById('globalnotify-drag-handle');
|
|
|
|
if (!widget || !handle) return;
|
|
|
|
handle.addEventListener('mousedown', function(e) {
|
|
if (e.target.closest('.globalnotify-action-link')) return;
|
|
|
|
GlobalNotify.isDragging = true;
|
|
var rect = widget.getBoundingClientRect();
|
|
GlobalNotify.dragOffset = {
|
|
x: e.clientX - rect.left,
|
|
y: e.clientY - rect.top
|
|
};
|
|
document.body.style.userSelect = 'none';
|
|
});
|
|
|
|
document.addEventListener('mousemove', function(e) {
|
|
if (!GlobalNotify.isDragging) return;
|
|
|
|
var x = e.clientX - GlobalNotify.dragOffset.x;
|
|
var y = e.clientY - GlobalNotify.dragOffset.y;
|
|
|
|
// Keep within viewport
|
|
x = Math.max(0, Math.min(x, window.innerWidth - 60));
|
|
y = Math.max(0, Math.min(y, window.innerHeight - 60));
|
|
|
|
widget.style.left = x + 'px';
|
|
widget.style.top = y + 'px';
|
|
widget.style.bottom = 'auto';
|
|
widget.style.right = 'auto';
|
|
});
|
|
|
|
document.addEventListener('mouseup', function() {
|
|
GlobalNotify.isDragging = false;
|
|
document.body.style.userSelect = '';
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Refresh notifications via AJAX
|
|
*/
|
|
refresh: function() {
|
|
this.ajaxCall('getcount', {}, function(data) {
|
|
if (data.success) {
|
|
GlobalNotify.updateBadgeFromServer(data.count);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Update badge from server count
|
|
*/
|
|
updateBadgeFromServer: function(count) {
|
|
var badge = document.querySelector('.globalnotify-fab-badge');
|
|
var fab = document.getElementById('globalnotify-fab');
|
|
|
|
if (count > 0) {
|
|
if (badge) {
|
|
badge.textContent = count;
|
|
} else if (fab) {
|
|
badge = document.createElement('span');
|
|
badge.className = 'globalnotify-fab-badge';
|
|
badge.textContent = count;
|
|
fab.appendChild(badge);
|
|
}
|
|
} else if (badge) {
|
|
badge.remove();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Initialize
|
|
*/
|
|
init: function() {
|
|
this.initDrag();
|
|
|
|
// Periodic refresh every 2 minutes
|
|
setInterval(function() {
|
|
GlobalNotify.refresh();
|
|
}, 120000);
|
|
}
|
|
};
|
|
|
|
// Initialize when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
GlobalNotify.init();
|
|
});
|