/** * 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 => `
`).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 ? ` ${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