kaauh_ats/static/js/messages.js
2025-11-18 14:05:28 +03:00

950 lines
28 KiB
JavaScript

/**
* Message System JavaScript
* Handles interactive features for the modern message interface
*/
class MessageSystem {
constructor() {
this.init();
}
init() {
this.initSearch();
this.initFolderNavigation();
this.initMessageActions();
this.initComposeFeatures();
this.initKeyboardShortcuts();
this.initAutoSave();
this.initAttachments();
this.initTooltips();
}
/**
* Initialize search functionality
*/
initSearch() {
const searchInputs = document.querySelectorAll('.search-input');
searchInputs.forEach(input => {
let searchTimeout;
input.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
const searchTerm = e.target.value.toLowerCase();
searchTimeout = setTimeout(() => {
this.performSearch(searchTerm);
}, 300);
});
// Clear search on Escape key
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
e.target.value = '';
this.performSearch('');
}
});
});
}
/**
* Perform search across message items
*/
performSearch(searchTerm) {
const messageItems = document.querySelectorAll('.message-item');
let visibleCount = 0;
messageItems.forEach(item => {
const text = item.textContent.toLowerCase();
if (text.includes(searchTerm)) {
item.style.display = 'flex';
visibleCount++;
} else {
item.style.display = 'none';
}
});
// Show no results message if needed
this.updateSearchResults(visibleCount, searchTerm);
}
/**
* Update search results display
*/
updateSearchResults(count, searchTerm) {
let noResults = document.querySelector('.search-no-results');
if (count === 0 && searchTerm) {
if (!noResults) {
noResults = document.createElement('div');
noResults.className = 'search-no-results';
noResults.innerHTML = `
<div class="text-center py-8">
<i class="fas fa-search text-4xl text-gray-400 mb-4"></i>
<h3 class="text-lg font-semibold text-gray-700 mb-2">
{% trans "No messages found" %}
</h3>
<p class="text-gray-500">
{% trans "No messages match your search for" %} "${searchTerm}"
</p>
</div>
`;
const messagesList = document.querySelector('.messages-list');
if (messagesList) {
messagesList.appendChild(noResults);
}
}
} else if (noResults) {
noResults.remove();
}
}
/**
* Initialize folder navigation
*/
initFolderNavigation() {
const folderItems = document.querySelectorAll('.folder-item');
folderItems.forEach(item => {
item.addEventListener('click', (e) => {
// Remove active class from all items
folderItems.forEach(f => f.classList.remove('active'));
// Add active class to clicked item
item.classList.add('active');
// Add loading state
this.showLoadingState();
// Navigate to folder (if it's a link)
if (item.tagName === 'A') {
// Let the link handle navigation
return;
}
});
});
}
/**
* Initialize message actions
*/
initMessageActions() {
// Refresh button
const refreshBtn = document.querySelector('.action-btn[title="Refresh"]');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.refreshMessages();
});
}
// Mark as read functionality
const markReadBtns = document.querySelectorAll('[onclick*="markAsRead"]');
markReadBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.markAsRead(btn);
});
});
// Delete message functionality
const deleteBtns = document.querySelectorAll('[onclick*="confirm"]');
deleteBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
if (!this.confirmDelete()) {
e.preventDefault();
}
});
});
}
/**
* Initialize compose features
*/
initComposeFeatures() {
const form = document.getElementById('composeForm');
if (!form) return;
// Auto-resize textarea
const textarea = form.querySelector('textarea[name="content"]');
if (textarea) {
textarea.addEventListener('input', () => {
this.autoResizeTextarea(textarea);
});
}
// Rich text toolbar
const toolbarBtns = document.querySelectorAll('.toolbar-btn');
toolbarBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.handleToolbarAction(btn);
});
});
// Save draft button
const saveDraftBtn = document.getElementById('saveDraftBtn');
if (saveDraftBtn) {
saveDraftBtn.addEventListener('click', () => {
this.saveDraft();
});
}
// Form submission
form.addEventListener('submit', (e) => {
this.handleFormSubmit(e);
});
}
/**
* Initialize keyboard shortcuts
*/
initKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + K for search
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
const searchInput = document.querySelector('.search-input');
if (searchInput) {
searchInput.focus();
}
}
// Ctrl/Cmd + N for new message
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
e.preventDefault();
const composeBtn = document.querySelector('.compose-btn');
if (composeBtn) {
window.location.href = composeBtn.href;
}
}
// Escape to close modals
if (e.key === 'Escape') {
this.closeModals();
}
});
}
/**
* Initialize auto-save functionality
*/
initAutoSave() {
const form = document.getElementById('composeForm');
if (!form) return;
let autoSaveTimer;
const inputs = form.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
input.addEventListener('input', () => {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(() => {
this.autoSave();
}, 30000); // Auto-save after 30 seconds
});
});
}
/**
* Initialize attachment handling
*/
initAttachments() {
const attachBtn = document.querySelector('.toolbar-btn[title*="Attach"]');
if (attachBtn) {
attachBtn.addEventListener('click', () => {
this.showAttachmentDialog();
});
}
}
/**
* Initialize tooltips
*/
initTooltips() {
const tooltipElements = document.querySelectorAll('[title]');
tooltipElements.forEach(element => {
element.addEventListener('mouseenter', (e) => {
this.showTooltip(e.target);
});
element.addEventListener('mouseleave', (e) => {
this.hideTooltip(e.target);
});
});
}
/**
* Refresh messages with animation
*/
refreshMessages() {
const refreshBtn = document.querySelector('.action-btn[title="Refresh"]');
if (refreshBtn) {
const icon = refreshBtn.querySelector('i');
icon.classList.add('fa-spin');
setTimeout(() => {
icon.classList.remove('fa-spin');
location.reload();
}, 1000);
}
}
/**
* Mark message as read
*/
async markAsRead(button) {
const messageId = button.getAttribute('data-message-id');
if (!messageId) return;
try {
const response = await fetch(`/messages/${messageId}/mark-read/`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (data.success) {
this.showNotification('{% trans "Message marked as read" %}', 'success');
location.reload();
} else {
this.showNotification('{% trans "Failed to mark message as read" %}', 'error');
}
} catch (error) {
console.error('Error marking message as read:', error);
this.showNotification('{% trans "An error occurred" %}', 'error');
}
}
/**
* Confirm delete action
*/
confirmDelete() {
return confirm('{% trans "Are you sure you want to delete this message?" %}');
}
/**
* Auto-resize textarea
*/
autoResizeTextarea(textarea) {
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
}
/**
* Handle toolbar actions
*/
handleToolbarAction(button) {
const action = button.getAttribute('title');
const textarea = document.querySelector('textarea[name="content"]');
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end);
switch (action) {
case '{% trans "Bold" %}':
this.wrapText(textarea, '**', '**');
break;
case '{% trans "Italic" %}':
this.wrapText(textarea, '*', '*');
break;
case '{% trans "Underline" %}':
this.wrapText(textarea, '__', '__');
break;
case '{% trans "Bullet List" %}':
this.insertList(textarea, '- ');
break;
case '{% trans "Numbered List" %}':
this.insertList(textarea, '1. ');
break;
case '{% trans "Insert Link" %}':
this.insertLink(textarea);
break;
case '{% trans "Insert Image" %}':
this.insertImage(textarea);
break;
case '{% trans "Attach File" %}':
this.showAttachmentDialog();
break;
}
}
/**
* Wrap selected text with formatting
*/
wrapText(textarea, before, after) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end);
const replacement = before + selectedText + after;
textarea.value = textarea.value.substring(0, start) + replacement + textarea.value.substring(end);
textarea.selectionStart = start + before.length;
textarea.selectionEnd = start + before.length + selectedText.length;
textarea.focus();
}
/**
* Insert list
*/
insertList(textarea, marker) {
const start = textarea.selectionStart;
const text = marker + '\n';
textarea.value = textarea.value.substring(0, start) + text + textarea.value.substring(start);
textarea.selectionStart = textarea.selectionEnd = start + text.length;
textarea.focus();
}
/**
* Insert link
*/
insertLink(textarea) {
const url = prompt('{% trans "Enter URL:" %}', 'https://');
if (url) {
const link = `[${url}](${url})`;
this.insertAtCursor(textarea, link);
}
}
/**
* Insert image
*/
insertImage(textarea) {
const url = prompt('{% trans "Enter image URL:" %}', 'https://');
if (url) {
const image = `![Image](${url})`;
this.insertAtCursor(textarea, image);
}
}
/**
* Insert text at cursor position
*/
insertAtCursor(textarea, text) {
const start = textarea.selectionStart;
textarea.value = textarea.value.substring(0, start) + text + textarea.value.substring(start);
textarea.selectionStart = textarea.selectionEnd = start + text.length;
textarea.focus();
}
/**
* Save draft
*/
async saveDraft() {
const form = document.getElementById('composeForm');
if (!form) return;
const formData = new FormData(form);
try {
const response = await fetch('/messages/save-draft/', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': this.getCSRFToken(),
},
});
const data = await response.json();
if (data.success) {
this.showNotification('{% trans "Draft saved" %}', 'success');
} else {
this.showNotification('{% trans "Failed to save draft" %}', 'error');
}
} catch (error) {
console.error('Error saving draft:', error);
this.showNotification('{% trans "An error occurred" %}', 'error');
}
}
/**
* Auto-save draft
*/
async autoSave() {
const form = document.getElementById('composeForm');
if (!form) return;
// Only auto-save if form has content
const hasContent = form.querySelector('textarea[name="content"]').value.trim() ||
form.querySelector('input[name="subject"]').value.trim();
if (!hasContent) return;
try {
const formData = new FormData(form);
await fetch('/messages/auto-save-draft/', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': this.getCSRFToken(),
},
});
console.log('Draft auto-saved');
} catch (error) {
console.error('Error auto-saving draft:', error);
}
}
/**
* Handle form submission
*/
handleFormSubmit(e) {
const form = e.target;
const submitBtn = form.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>{% trans "Sending..." %}';
}
}
/**
* Show attachment dialog
*/
showAttachmentDialog() {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.accept = 'image/*,.pdf,.doc,.docx,.txt';
input.addEventListener('change', (e) => {
this.handleFileSelect(e.target.files);
});
input.click();
}
/**
* Handle file selection
*/
handleFileSelect(files) {
const attachmentsSection = document.getElementById('attachmentsSection');
const attachmentList = document.getElementById('attachmentList');
if (!attachmentsSection || !attachmentList) return;
attachmentsSection.style.display = 'block';
Array.from(files).forEach(file => {
const attachmentItem = this.createAttachmentItem(file);
attachmentList.appendChild(attachmentItem);
});
}
/**
* Create attachment item
*/
createAttachmentItem(file) {
const item = document.createElement('div');
item.className = 'attachment-item';
const icon = this.getFileIcon(file.type);
const size = this.formatFileSize(file.size);
item.innerHTML = `
<i class="fas ${icon} attachment-icon"></i>
<span class="attachment-name">${file.name}</span>
<span class="attachment-size">${size}</span>
<i class="fas fa-times attachment-remove" onclick="this.parentElement.remove()"></i>
`;
return item;
}
/**
* Get file icon based on MIME type
*/
getFileIcon(mimeType) {
if (mimeType.startsWith('image/')) return 'fa-image';
if (mimeType.includes('pdf')) return 'fa-file-pdf';
if (mimeType.includes('word')) return 'fa-file-word';
if (mimeType.includes('text')) return 'fa-file-alt';
return 'fa-file';
}
/**
* Format file size
*/
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];
}
/**
* Show loading state
*/
showLoadingState() {
const messagesList = document.querySelector('.messages-list');
if (messagesList) {
messagesList.style.opacity = '0.5';
}
}
/**
* Show notification
*/
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<i class="fas ${this.getNotificationIcon(type)} me-2"></i>
${message}
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('show');
}, 100);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
notification.remove();
}, 300);
}, 3000);
}
/**
* Get notification icon
*/
getNotificationIcon(type) {
const icons = {
success: 'fa-check-circle',
error: 'fa-exclamation-circle',
warning: 'fa-exclamation-triangle',
info: 'fa-info-circle'
};
return icons[type] || icons.info;
}
/**
* Show tooltip
*/
showTooltip(element) {
const title = element.getAttribute('title');
if (!title) return;
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.textContent = title;
document.body.appendChild(tooltip);
const rect = element.getBoundingClientRect();
tooltip.style.left = rect.left + (rect.width / 2) - (tooltip.offsetWidth / 2) + 'px';
tooltip.style.top = rect.top - tooltip.offsetHeight - 5 + 'px';
element.setAttribute('data-original-title', title);
element.removeAttribute('title');
}
/**
* Hide tooltip
*/
hideTooltip(element) {
const tooltip = document.querySelector('.tooltip');
if (tooltip) {
tooltip.remove();
}
const originalTitle = element.getAttribute('data-original-title');
if (originalTitle) {
element.setAttribute('title', originalTitle);
element.removeAttribute('data-original-title');
}
}
/**
* Close modals
*/
closeModals() {
const modals = document.querySelectorAll('.modal');
modals.forEach(modal => {
modal.style.display = 'none';
});
}
/**
* Get CSRF token
*/
getCSRFToken() {
const cookie = document.cookie.split(';').find(c => c.trim().startsWith('csrftoken='));
return cookie ? cookie.split('=')[1] : '';
}
/**
* Initialize reply functionality
*/
initReplyFunctionality() {
// Handle reply button clicks
const replyBtns = document.querySelectorAll('.reply-btn');
replyBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.handleReplyClick(btn);
});
});
// Handle reply form submissions
const replyForms = document.querySelectorAll('#reply-form');
replyForms.forEach(form => {
form.addEventListener('submit', (e) => {
e.preventDefault();
this.handleReplySubmit(form);
});
});
// Handle cancel button in reply forms
const cancelBtns = document.querySelectorAll('[onclick*="hideReplyForm"]');
cancelBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.hideReplyForm(btn.closest('#reply-section'));
});
});
}
/**
* Handle reply button click
*/
async handleReplyClick(button) {
const messageId = button.getAttribute('data-message-id');
if (!messageId) return;
try {
// Show loading state
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Loading...';
button.disabled = true;
// Fetch reply form via AJAX
const response = await fetch(`/messages/${messageId}/reply/`, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': this.getCSRFToken(),
},
});
const data = await response.json();
if (data.success) {
// Show reply form
const replySection = document.getElementById('reply-section');
if (replySection) {
replySection.innerHTML = data.html;
// Focus on the textarea
const textarea = replySection.querySelector('textarea[name="content"]');
if (textarea) {
setTimeout(() => textarea.focus(), 100);
}
}
} else {
this.showNotification('Failed to load reply form', 'error');
}
// Restore button state
button.innerHTML = originalText;
button.disabled = false;
} catch (error) {
console.error('Error loading reply form:', error);
this.showNotification('An error occurred while loading reply form', 'error');
// Restore button state
button.innerHTML = originalText;
button.disabled = false;
}
}
/**
* Handle reply form submission
*/
async handleReplySubmit(form) {
const submitBtn = form.querySelector('button[type="submit"]');
const textarea = form.querySelector('textarea[name="content"]');
const content = textarea.value.trim();
if (!content) {
this.showNotification('Reply content cannot be empty', 'error');
textarea.focus();
return;
}
try {
// Show loading state
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Sending...';
submitBtn.disabled = true;
const formData = new FormData(form);
const response = await fetch(form.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': this.getCSRFToken(),
},
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
// Add the reply to the conversation (if on detail page)
this.addReplyToConversation(data);
// Hide the reply form
this.hideReplyForm(form.closest('#reply-section'));
} else {
this.showNotification(data.error || 'Failed to send reply', 'error');
}
// Restore button state
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
} catch (error) {
console.error('Error sending reply:', error);
this.showNotification('An error occurred while sending reply', 'error');
// Restore button state
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
}
/**
* Add reply to conversation
*/
addReplyToConversation(replyData) {
const conversationContainer = document.querySelector('.conversation-container');
if (!conversationContainer) return;
// Create new reply element
const replyElement = document.createElement('div');
replyElement.className = 'message-reply fade-in';
replyElement.innerHTML = `
<div class="flex items-start space-x-3 mb-4">
<div class="flex-shrink-0">
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center">
<span class="text-white text-sm font-medium">${replyData.sender_name || 'You'}</span>
</div>
</div>
<div class="flex-grow">
<div class="bg-gray-50 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-900">Reply</h4>
<span class="text-xs text-gray-500">${replyData.reply_time}</span>
</div>
<p class="text-sm text-gray-700 whitespace-pre-wrap">${replyData.reply_content}</p>
</div>
</div>
</div>
`;
conversationContainer.appendChild(replyElement);
// Scroll to the new reply
replyElement.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
/**
* Hide reply form
*/
hideReplyForm(replySection) {
if (replySection) {
replySection.innerHTML = '';
}
}
}
// Initialize the message system when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.messageSystem = new MessageSystem();
// Initialize reply functionality
if (window.messageSystem) {
window.messageSystem.initReplyFunctionality();
}
});
// Add notification styles
const notificationStyles = `
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 8px;
color: white;
font-weight: 500;
z-index: 9999;
transform: translateX(100%);
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.notification.show {
transform: translateX(0);
}
.notification-success {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
}
.notification-error {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
}
.notification-warning {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
}
.notification-info {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
}
.tooltip {
position: absolute;
background: #1f2937;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
z-index: 9999;
pointer-events: none;
}
.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #1f2937;
}
`;
// Add styles to head
const styleSheet = document.createElement('style');
styleSheet.textContent = notificationStyles;
document.head.appendChild(styleSheet);