dolibarr.globalnotify/js/globalnotify.js
data c65d15a86b feat: GlobalNotify v1.1.0 - Schwebendes Benachrichtigungs-Widget
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>
2026-02-23 11:04:43 +01:00

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();
});