This commit is contained in:
2026-05-13 17:11:09 +02:00
commit ea63897455
2785 changed files with 359868 additions and 0 deletions

View File

@@ -0,0 +1,595 @@
/**
* Messaging JavaScript Component
* Handles message composition, attachments, and interface interactions
*/
// Prevent duplicate declaration
if (typeof MessagingManager === 'undefined') {
class MessagingManager {
constructor(options = {}) {
this.options = {
maxAttachmentSize: options.maxAttachmentSize || 10, // MB
allowedFileTypes: options.allowedFileTypes || ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'jpeg', 'png', 'gif'],
uploadUrl: options.uploadUrl || '/admin/messages/upload-attachment',
removeAttachmentUrl: options.removeAttachmentUrl || '/admin/messages/remove-attachment',
sendMessageUrl: options.sendMessageUrl || '/admin/messages/send',
...options
};
this.attachments = [];
this.tempAttachmentIds = [];
this.init();
}
init() {
this.setupElements();
this.bindEvents();
this.initializeRichEditor();
}
setupElements() {
this.messageForm = document.getElementById('message-form');
this.recipientsSelect = document.getElementById('recipients');
this.subjectInput = document.getElementById('subject');
this.contentEditor = document.getElementById('content');
this.attachmentInput = document.getElementById('attachment-input');
this.attachmentsList = document.getElementById('attachments-list');
this.requireAcknowledgment = document.getElementById('require_acknowledgment');
this.sendButton = document.getElementById('send-message-btn');
this.saveDraftButton = document.getElementById('save-draft-btn');
this.attachmentButton = document.getElementById('attachment-btn');
}
bindEvents() {
// File attachment
if (this.attachmentInput) {
this.attachmentInput.addEventListener('change', (e) => {
this.handleFileSelection(e.target.files);
});
}
if (this.attachmentButton) {
this.attachmentButton.addEventListener('click', (e) => {
e.preventDefault();
this.attachmentInput?.click();
});
}
// Form submission
if (this.messageForm) {
this.messageForm.addEventListener('submit', (e) => {
e.preventDefault();
this.sendMessage();
});
}
// Save draft
if (this.saveDraftButton) {
this.saveDraftButton.addEventListener('click', (e) => {
e.preventDefault();
this.saveDraft();
});
}
// Auto-save draft every 30 seconds
setInterval(() => {
this.autoSaveDraft();
}, 30000);
// Recipients validation
if (this.recipientsSelect) {
this.recipientsSelect.addEventListener('change', () => {
this.validateForm();
});
}
// Subject validation
if (this.subjectInput) {
this.subjectInput.addEventListener('input', () => {
this.validateForm();
});
}
}
initializeRichEditor() {
if (this.contentEditor && typeof tinymce !== 'undefined') {
tinymce.init({
selector: '#content',
height: 300,
menubar: false,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount'
],
toolbar: 'undo redo | formatselect | bold italic backcolor | \
alignleft aligncenter alignright alignjustify | \
bullist numlist outdent indent | removeformat | help',
setup: (editor) => {
editor.on('change', () => {
this.validateForm();
});
}
});
}
}
async handleFileSelection(files) {
for (let file of files) {
if (this.validateFile(file)) {
await this.uploadFile(file);
}
}
// Clear the input
if (this.attachmentInput) {
this.attachmentInput.value = '';
}
}
validateFile(file) {
// Check file size
const maxSizeBytes = this.options.maxAttachmentSize * 1024 * 1024;
if (file.size > maxSizeBytes) {
this.showError(trans('admin.ATTACHMENT_TOO_LARGE'));
return false;
}
// Check file type
const extension = file.name.split('.').pop().toLowerCase();
if (!this.options.allowedFileTypes.includes(extension)) {
this.showError(trans('admin.INVALID_FILE_TYPE'));
return false;
}
return true;
}
async uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
try {
this.showUploadProgress(file.name);
const response = await fetch(this.options.uploadUrl, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
},
body: formData
});
const data = await response.json();
if (response.ok && data.success) {
this.addAttachment(data.attachment);
this.tempAttachmentIds.push(data.attachment.id);
} else {
this.showError(data.message || trans('admin.UPLOAD_FAILED'));
}
} catch (error) {
console.error('Upload error:', error);
this.showError(trans('admin.UPLOAD_FAILED'));
} finally {
this.hideUploadProgress();
}
}
addAttachment(attachment) {
this.attachments.push(attachment);
this.renderAttachments();
}
async removeAttachment(attachmentId) {
try {
const response = await fetch(this.options.removeAttachmentUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
},
body: JSON.stringify({ id: attachmentId })
});
if (response.ok) {
this.attachments = this.attachments.filter(att => att.id !== attachmentId);
this.tempAttachmentIds = this.tempAttachmentIds.filter(id => id !== attachmentId);
this.renderAttachments();
}
} catch (error) {
console.error('Remove attachment error:', error);
}
}
renderAttachments() {
if (!this.attachmentsList) return;
if (this.attachments.length === 0) {
this.attachmentsList.innerHTML = '';
return;
}
const attachmentsHtml = this.attachments.map(attachment => `
<div class="attachment-item d-flex align-items-center justify-content-between p-2 border rounded mb-2">
<div class="d-flex align-items-center">
<i class="${this.getFileIcon(attachment.file_name)} me-2"></i>
<span class="file-name">${this.escapeHtml(attachment.original_name)}</span>
<span class="file-size text-muted ms-2">(${this.formatFileSize(attachment.file_size)})</span>
</div>
<button type="button" class="btn btn-sm btn-outline-danger"
onclick="messagingManager.removeAttachment(${attachment.id})">
<i class="fas fa-times"></i>
</button>
</div>
`).join('');
this.attachmentsList.innerHTML = attachmentsHtml;
}
getFileIcon(fileName) {
const extension = fileName.split('.').pop().toLowerCase();
const icons = {
'pdf': 'fas fa-file-pdf text-danger',
'doc': 'fas fa-file-word text-primary',
'docx': 'fas fa-file-word text-primary',
'xls': 'fas fa-file-excel text-success',
'xlsx': 'fas fa-file-excel text-success',
'jpg': 'fas fa-file-image text-info',
'jpeg': 'fas fa-file-image text-info',
'png': 'fas fa-file-image text-info',
'gif': 'fas fa-file-image text-info'
};
return icons[extension] || 'fas fa-file text-secondary';
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
async sendMessage() {
if (!this.validateForm()) return;
const formData = this.getFormData();
formData.attachment_ids = this.tempAttachmentIds;
try {
this.setSendingState(true);
const response = await fetch(this.options.sendMessageUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
},
body: JSON.stringify(formData)
});
const data = await response.json();
if (response.ok && data.success) {
this.showSuccess(trans('admin.MESSAGE_SENT'));
this.resetForm();
// Redirect to inbox or sent messages
setTimeout(() => {
window.location.href = data.redirect || '/admin/messages';
}, 1500);
} else {
this.showError(data.message || trans('admin.SEND_FAILED'));
}
} catch (error) {
console.error('Send message error:', error);
this.showError(trans('admin.SEND_FAILED'));
} finally {
this.setSendingState(false);
}
}
async saveDraft() {
const formData = this.getFormData();
formData.is_draft = true;
formData.attachment_ids = this.tempAttachmentIds;
try {
const response = await fetch(this.options.sendMessageUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
},
body: JSON.stringify(formData)
});
const data = await response.json();
if (response.ok && data.success) {
this.showSuccess(trans('admin.DRAFT_SAVED'));
}
} catch (error) {
console.error('Save draft error:', error);
}
}
autoSaveDraft() {
const hasContent = this.hasFormContent();
if (hasContent) {
this.saveDraft();
}
}
getFormData() {
const recipients = Array.from(this.recipientsSelect?.selectedOptions || [])
.map(option => option.value);
const content = this.contentEditor && typeof tinymce !== 'undefined'
? tinymce.get('content')?.getContent() || ''
: this.contentEditor?.value || '';
return {
recipients: recipients,
subject: this.subjectInput?.value || '',
content: content,
require_acknowledgment: this.requireAcknowledgment?.checked || false,
attachment_ids: this.tempAttachmentIds
};
} validateForm() {
const data = this.getFormData();
const maxRecipients = window.messagingConfig?.maxRecipients || 20;
let isValid = data.recipients.length > 0 &&
data.subject.trim() !== '' &&
data.content.trim() !== '';
// Check max recipients limit
if (data.recipients.length > maxRecipients) {
isValid = false;
this.showError(`Maximum ${maxRecipients} recipients allowed.`);
}
if (this.sendButton) {
this.sendButton.disabled = !isValid;
}
return isValid;
}
hasFormContent() {
const data = this.getFormData();
return data.recipients.length > 0 ||
data.subject.trim() !== '' ||
data.content.trim() !== '' ||
this.attachments.length > 0;
}
resetForm() {
if (this.recipientsSelect) {
Array.from(this.recipientsSelect.options).forEach(option => {
option.selected = false;
});
}
if (this.subjectInput) this.subjectInput.value = '';
if (this.contentEditor && typeof tinymce !== 'undefined') {
tinymce.get('content')?.setContent('');
} else if (this.contentEditor) {
this.contentEditor.value = '';
}
if (this.requireAcknowledgment) {
this.requireAcknowledgment.checked = false;
}
this.attachments = [];
this.tempAttachmentIds = [];
this.renderAttachments();
this.validateForm();
}
setSendingState(sending) {
if (this.sendButton) {
this.sendButton.disabled = sending;
this.sendButton.innerHTML = sending
? `<i class="fas fa-spinner fa-spin"></i> ${trans('admin.SENDING')}`
: trans('admin.SEND_MESSAGE');
}
}
showUploadProgress(fileName) {
// You can implement a progress indicator here
console.log(`Uploading ${fileName}...`);
}
hideUploadProgress() {
// Hide progress indicator
}
showSuccess(message) {
// Implement success notification
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: 'success',
title: trans('admin.SUCCESS'),
text: message,
timer: 3000,
showConfirmButton: false
});
} else {
alert(message);
}
}
showError(message) {
// Implement error notification
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: 'error',
title: trans('admin.ERROR'),
text: message
});
} else {
alert(message);
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Bulk actions for message lists
class MessageBulkActions {
constructor() {
this.selectedMessages = new Set();
this.init();
}
init() {
this.bindEvents();
}
bindEvents() {
// Select all checkbox
const selectAllCheckbox = document.getElementById('select-all-messages');
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', (e) => {
this.selectAll(e.target.checked);
});
}
// Individual message checkboxes
document.addEventListener('change', (e) => {
if (e.target.matches('.message-checkbox')) {
this.toggleMessage(e.target.value, e.target.checked);
}
});
// Bulk action buttons
document.addEventListener('click', (e) => {
if (e.target.matches('#bulk-mark-read')) {
e.preventDefault();
this.bulkMarkRead();
} else if (e.target.matches('#bulk-star')) {
e.preventDefault();
this.bulkStar();
} else if (e.target.matches('#bulk-archive')) {
e.preventDefault();
this.bulkArchive();
} else if (e.target.matches('#bulk-delete')) {
e.preventDefault();
this.bulkDelete();
}
});
}
selectAll(checked) {
const checkboxes = document.querySelectorAll('.message-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = checked;
this.toggleMessage(checkbox.value, checked);
});
}
toggleMessage(messageId, selected) {
if (selected) {
this.selectedMessages.add(messageId);
} else {
this.selectedMessages.delete(messageId);
}
this.updateBulkActionsVisibility();
}
updateBulkActionsVisibility() {
const bulkActions = document.getElementById('bulk-actions');
const hasSelected = this.selectedMessages.size > 0;
if (bulkActions) {
bulkActions.style.display = hasSelected ? 'block' : 'none';
}
}
async bulkMarkRead() {
await this.performBulkAction('mark-read');
}
async bulkStar() {
await this.performBulkAction('star');
}
async bulkArchive() {
await this.performBulkAction('archive');
}
async bulkDelete() {
if (!confirm(trans('admin.CONFIRM_DELETE_SELECTED'))) return;
await this.performBulkAction('delete');
}
async performBulkAction(action) {
if (this.selectedMessages.size === 0) return;
try {
const response = await fetch('/admin/messages/bulk-action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
},
body: JSON.stringify({
action: action,
message_ids: Array.from(this.selectedMessages)
})
});
if (response.ok) {
window.location.reload();
} else {
throw new Error('Bulk action failed');
}
} catch (error) {
console.error('Bulk action error:', error);
alert(trans('admin.BULK_ACTION_FAILED'));
}
}
}
// Global helper function for translations
function trans(key, replacements = {}) {
const translations = window.translations || {};
let translation = translations[key] || key;
Object.keys(replacements).forEach(search => {
translation = translation.replace(`:${search}`, replacements[search]);
});
return translation;
}
// Auto-initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
if (document.getElementById('message-form')) {
window.messagingManager = new MessagingManager();
}
if (document.querySelector('.message-checkbox')) {
window.messageBulkActions = new MessageBulkActions();
}
});
// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = { MessagingManager, MessageBulkActions };
}
} // End of MessagingManager class guard

View File

@@ -0,0 +1,453 @@
/**
* Real-time Notifications JavaScript Component
* Handles notification polling, desktop notifications, and UI updates
*/
class NotificationManager {
constructor(options = {}) {
this.options = {
pollInterval: options.pollInterval || 30000, // 30 seconds
enableDesktopNotifications: options.enableDesktopNotifications || true,
enableSound: options.enableSound || false, // Disable sound by default to avoid 404
soundUrl: options.soundUrl || '/admin/sounds/notification.mp3',
markReadUrl: options.markReadUrl || '/admin/notifications/mark-read',
markAllReadUrl: options.markAllReadUrl || '/admin/notifications/mark-all-read',
fetchUrl: options.fetchUrl || '/admin/notifications/fetch',
...options
};
this.isPolling = false;
this.unreadCount = 0;
this.lastNotificationTime = null;
this.audio = null;
this.init();
}
init() {
this.setupElements();
this.requestDesktopPermission();
this.setupAudio();
this.bindEvents();
this.startPolling();
this.loadNotifications();
}
setupElements() {
this.notificationButton = document.getElementById('notification-button');
this.notificationDropdown = document.getElementById('notification-dropdown');
this.notificationCount = document.getElementById('notification-count');
this.notificationList = document.getElementById('notification-list');
this.markAllReadBtn = document.getElementById('mark-all-read');
if (!this.notificationButton || !this.notificationDropdown) {
console.warn('Notification elements not found');
return false;
}
return true;
}
requestDesktopPermission() {
if (this.options.enableDesktopNotifications && 'Notification' in window) {
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}
} setupAudio() {
if (this.options.enableSound) {
try {
this.audio = new Audio(this.options.soundUrl);
this.audio.preload = 'auto';
// Handle loading errors
this.audio.addEventListener('error', (e) => {
console.warn('Notification sound file not found or could not be loaded:', this.options.soundUrl);
this.audio = null; // Disable audio if it can't load
});
} catch (error) {
console.warn('Could not setup notification audio:', error);
this.audio = null;
}
}
} bindEvents() {
// Mark all as read
if (this.markAllReadBtn) {
this.markAllReadBtn.addEventListener('click', (e) => {
e.preventDefault();
this.markAllAsRead();
});
}
// Listen to Bootstrap dropdown events
if (this.notificationButton) {
// Try multiple event binding approaches for different Bootstrap versions
// Bootstrap 4/5 events
this.notificationButton.addEventListener('show.bs.dropdown', () => {
this.loadNotifications();
});
// jQuery-based Bootstrap events (fallback)
if (typeof $ !== 'undefined') {
$(this.notificationButton).on('show.bs.dropdown', () => {
this.loadNotifications();
});
// Also try the older Bootstrap 3 events
$(this.notificationButton).on('shown.bs.dropdown', () => {
this.loadNotifications();
});
}
// Fallback: still handle click events but don't prevent default
this.notificationButton.addEventListener('click', () => {
// Small delay to ensure dropdown state is updated
setTimeout(() => {
this.loadNotifications();
}, 100);
});
}
// Handle individual notification clicks
if (this.notificationList) {
this.notificationList.addEventListener('click', (e) => {
const notificationItem = e.target.closest('.notification-item');
if (notificationItem) {
const notificationId = notificationItem.dataset.notificationId;
if (notificationId) {
this.markAsRead(notificationId);
}
}
});
}
}
startPolling() {
if (this.isPolling) return;
this.isPolling = true;
this.pollInterval = setInterval(() => {
this.loadNotifications();
}, this.options.pollInterval);
}
stopPolling() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
this.isPolling = false;
}
async loadNotifications() {
try {
const response = await fetch(this.options.fetchUrl, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
this.updateNotifications(data);
} catch (error) {
console.error('Failed to load notifications:', error);
}
}
updateNotifications(data) {
const newUnreadCount = data.unread_count || 0;
const notifications = data.notifications || [];
// Check for new notifications
const hasNewNotifications = newUnreadCount > this.unreadCount;
// Update count
this.unreadCount = newUnreadCount;
this.updateUnreadCount();
// Update notification list
this.updateNotificationList(notifications);
// Show desktop notifications for new items
if (hasNewNotifications && notifications.length > 0) {
const latestNotification = notifications[0];
if (this.shouldShowDesktopNotification(latestNotification)) {
this.showDesktopNotification(latestNotification);
this.playSound();
}
}
}
updateUnreadCount() {
if (this.notificationCount) {
if (this.unreadCount > 0) {
// Format count: show 99+ for counts over 99
const displayCount = this.unreadCount > 99 ? '99+' : this.unreadCount.toString();
this.notificationCount.textContent = displayCount;
this.notificationCount.classList.remove('d-none');
this.notificationButton?.classList.add('has-notifications');
} else {
this.notificationCount.classList.add('d-none');
this.notificationButton?.classList.remove('has-notifications');
}
}
}
updateNotificationList(notifications) {
if (!this.notificationList) return;
if (notifications.length === 0) {
this.notificationList.innerHTML = `
<div class="dropdown-item text-center text-muted py-3">
<i class="fas fa-bell-slash mb-2"></i><br>
${trans('admin.NO_NOTIFICATIONS')}
</div>
`;
return;
}
const notificationHtml = notifications.map(notification =>
this.renderNotification(notification)
).join('');
this.notificationList.innerHTML = notificationHtml;
}
renderNotification(notification) {
const isRead = notification.read_at;
const timeAgo = this.timeAgo(notification.created_at);
const iconClass = this.getNotificationIcon(notification.type, notification.data);
const typeClass = `type-${notification.type}`;
// Add special class for tardiness alerts
const isTardinessAlert = notification.type === 'system_alert' && notification.data && notification.data.type === 'tardiness_alert';
const specialClass = isTardinessAlert ? 'type-tardiness_alert' : typeClass;
return `
<div class="dropdown-item notification-item ${isRead ? 'read' : 'unread'}"
data-notification-id="${notification.id}">
<div class="d-flex align-items-start">
<div class="notification-icon ${specialClass} me-3">
<i class="${iconClass}"></i>
</div>
<div class="notification-content flex-grow-1">
<div class="notification-title">${this.escapeHtml(notification.title)}</div>
<div class="notification-message">${this.escapeHtml(notification.message)}</div>
<div class="notification-time text-muted small">
<i class="fas fa-clock"></i> ${timeAgo}
</div>
</div>
${!isRead ? '<div class="notification-badge"></div>' : ''}
</div>
</div>
`;
}
getNotificationIcon(type, data = {}) {
// Check if it's a system_alert with specific subtype
if (type === 'system_alert' && data && data.type === 'tardiness_alert') {
return 'fas fa-clock text-danger';
}
const icons = {
'message': 'fas fa-envelope',
'system_alert': 'fas fa-exclamation-triangle',
'plugin_notification': 'fas fa-puzzle-piece',
'system': 'fas fa-cog',
'warning': 'fas fa-exclamation-triangle',
'info': 'fas fa-info-circle',
'success': 'fas fa-check-circle',
'error': 'fas fa-times-circle',
'tardiness_alert': 'fas fa-clock text-danger'
};
return icons[type] || 'fas fa-bell';
}
shouldShowDesktopNotification(notification) {
if (!this.options.enableDesktopNotifications || Notification.permission !== 'granted') {
return false;
}
// Don't show if notification is older than last check
if (this.lastNotificationTime && new Date(notification.created_at) <= this.lastNotificationTime) {
return false;
}
return true;
}
showDesktopNotification(notification) {
if (Notification.permission === 'granted') {
const desktopNotification = new Notification(notification.title, {
body: notification.message,
icon: '/admin/images/notification-icon.png',
tag: `notification-${notification.id}`
});
desktopNotification.onclick = () => {
window.focus();
this.markAsRead(notification.id);
desktopNotification.close();
};
// Auto close after 5 seconds
setTimeout(() => {
desktopNotification.close();
}, 5000);
}
}
playSound() {
if (this.options.enableSound && this.audio) {
this.audio.play().catch(error => {
console.warn('Could not play notification sound:', error);
});
}
} async markAsRead(notificationId) {
try {
// Build the URL with the notification ID parameter
const url = this.options.markReadUrl.replace('__ID__', notificationId);
const response = await fetch(url, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
}
});
if (response.ok) {
// Update UI to mark as read
const notificationItem = document.querySelector(`[data-notification-id="${notificationId}"]`);
if (notificationItem) {
notificationItem.classList.remove('unread');
notificationItem.classList.add('read');
const badge = notificationItem.querySelector('.notification-badge');
if (badge) badge.remove();
}
// Decrease unread count
if (this.unreadCount > 0) {
this.unreadCount--;
this.updateUnreadCount();
}
}
} catch (error) {
console.error('Failed to mark notification as read:', error);
}
}
async markAllAsRead() {
try {
const response = await fetch(this.options.markAllReadUrl, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
}
});
if (response.ok) {
// Update all notifications to read state
const unreadItems = document.querySelectorAll('.notification-item.unread');
unreadItems.forEach(item => {
item.classList.remove('unread');
item.classList.add('read');
const badge = item.querySelector('.notification-badge');
if (badge) badge.remove();
});
// Reset unread count
this.unreadCount = 0;
this.updateUnreadCount();
}
} catch (error) {
console.error('Failed to mark all notifications as read:', error);
}
}
toggleDropdown() {
// Let Bootstrap handle the dropdown toggle naturally
// Just load notifications when needed
this.loadNotifications();
}
openDropdown() {
// Bootstrap handles the visual state, we just ensure data is loaded
this.loadNotifications();
}
closeDropdown() {
// Bootstrap handles the visual state
// No additional action needed
}
timeAgo(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffInSeconds < 60) {
return trans('admin.JUST_NOW');
} else if (diffInSeconds < 3600) {
const minutes = Math.floor(diffInSeconds / 60);
return trans('admin.MINUTES_AGO').replace(':minutes', minutes);
} else if (diffInSeconds < 86400) {
const hours = Math.floor(diffInSeconds / 3600);
return trans('admin.HOURS_AGO').replace(':hours', hours);
} else {
const days = Math.floor(diffInSeconds / 86400);
return trans('admin.DAYS_AGO').replace(':days', days);
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
destroy() {
this.stopPolling();
// Remove event listeners if needed
}
}
// Global helper function for translations
function trans(key, replacements = {}) {
// This should be implemented based on your translation system
// For now, return the key as fallback
const translations = window.translations || {};
let translation = translations[key] || key;
Object.keys(replacements).forEach(search => {
translation = translation.replace(`:${search}`, replacements[search]);
});
return translation;
}
// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = NotificationManager;
}
document.addEventListener('DOMContentLoaded', function() {
// Get configuration from backend (passed via window.notificationConfig)
const config = window.notificationConfig || {};
// Wait for NotificationManager to be available before initializing
window.notificationManager = new NotificationManager(config);
});