566 lines
22 KiB
HTML
566 lines
22 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}Inbox - {{ block.super }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-1">Inbox</h1>
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb mb-0">
|
|
<li class="breadcrumb-item"><a href="{% url 'communications:dashboard' %}">Communications</a></li>
|
|
<li class="breadcrumb-item active">Inbox</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
<div class="btn-group">
|
|
<a href="{% url 'communications:message_compose' %}" class="btn btn-primary">
|
|
<i class="fas fa-plus me-2"></i>Compose
|
|
</a>
|
|
<button type="button" class="btn btn-outline-primary" onclick="refreshInbox()">
|
|
<i class="fas fa-sync-alt me-2"></i>Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Sidebar -->
|
|
<div class="col-lg-3 mb-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-folder me-2"></i>Folders
|
|
</h5>
|
|
</div>
|
|
<div class="list-group list-group-flush">
|
|
<a href="{% url 'communications:message_inbox' %}" class="list-group-item list-group-item-action active">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span><i class="fas fa-inbox me-2"></i>Inbox</span>
|
|
<span class="badge bg-primary rounded-pill" id="inbox-count">{{ inbox_count|default:0 }}</span>
|
|
</div>
|
|
</a>
|
|
<a href="{% url 'communications:message_sent' %}" class="list-group-item list-group-item-action">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span><i class="fas fa-paper-plane me-2"></i>Sent</span>
|
|
<span class="badge bg-secondary rounded-pill">{{ sent_count|default:0 }}</span>
|
|
</div>
|
|
</a>
|
|
<a href="{% url 'communications:message_drafts' %}" class="list-group-item list-group-item-action">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span><i class="fas fa-edit me-2"></i>Drafts</span>
|
|
<span class="badge bg-warning rounded-pill">{{ drafts_count|default:0 }}</span>
|
|
</div>
|
|
</a>
|
|
<a href="{% url 'communications:message_starred' %}" class="list-group-item list-group-item-action">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span><i class="fas fa-star me-2"></i>Starred</span>
|
|
<span class="badge bg-info rounded-pill">{{ starred_count|default:0 }}</span>
|
|
</div>
|
|
</a>
|
|
<a href="{% url 'communications:message_archived' %}" class="list-group-item list-group-item-action">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span><i class="fas fa-archive me-2"></i>Archived</span>
|
|
<span class="badge bg-secondary rounded-pill">{{ archived_count|default:0 }}</span>
|
|
</div>
|
|
</a>
|
|
<a href="{% url 'communications:message_trash' %}" class="list-group-item list-group-item-action">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span><i class="fas fa-trash me-2"></i>Trash</span>
|
|
<span class="badge bg-danger rounded-pill">{{ trash_count|default:0 }}</span>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Filters -->
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-filter me-2"></i>Quick Filters
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-grid gap-2">
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="filterMessages('unread')">
|
|
<i class="fas fa-envelope me-2"></i>Unread
|
|
</button>
|
|
<button type="button" class="btn btn-outline-success btn-sm" onclick="filterMessages('urgent')">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>Urgent
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info btn-sm" onclick="filterMessages('attachments')">
|
|
<i class="fas fa-paperclip me-2"></i>With Attachments
|
|
</button>
|
|
<button type="button" class="btn btn-outline-warning btn-sm" onclick="filterMessages('today')">
|
|
<i class="fas fa-calendar-day me-2"></i>Today
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="clearFilters()">
|
|
<i class="fas fa-times me-2"></i>Clear Filters
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="col-lg-9">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-6">
|
|
<div class="d-flex align-items-center">
|
|
<div class="form-check me-3">
|
|
<input class="form-check-input" type="checkbox" id="selectAll" onchange="toggleSelectAll()">
|
|
<label class="form-check-label" for="selectAll">
|
|
Select All
|
|
</label>
|
|
</div>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-primary" onclick="markAsRead()" disabled id="markReadBtn">
|
|
<i class="fas fa-envelope-open me-1"></i>Mark Read
|
|
</button>
|
|
<button type="button" class="btn btn-outline-warning" onclick="markAsUnread()" disabled id="markUnreadBtn">
|
|
<i class="fas fa-envelope me-1"></i>Mark Unread
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info" onclick="starMessages()" disabled id="starBtn">
|
|
<i class="fas fa-star me-1"></i>Star
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="archiveMessages()" disabled id="archiveBtn">
|
|
<i class="fas fa-archive me-1"></i>Archive
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger" onclick="deleteMessages()" disabled id="deleteBtn">
|
|
<i class="fas fa-trash me-1"></i>Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="d-flex justify-content-end align-items-center">
|
|
<div class="input-group input-group-sm me-3" style="max-width: 300px;">
|
|
<input type="text" class="form-control" placeholder="Search messages..." id="searchInput" onkeyup="searchMessages()">
|
|
<button class="btn btn-outline-secondary" type="button" onclick="searchMessages()">
|
|
<i class="fas fa-search"></i>
|
|
</button>
|
|
</div>
|
|
<div class="dropdown">
|
|
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-sort me-1"></i>Sort
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="#" onclick="sortMessages('date_desc')">Newest First</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="sortMessages('date_asc')">Oldest First</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="sortMessages('sender')">By Sender</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="sortMessages('subject')">By Subject</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="sortMessages('priority')">By Priority</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div id="messages-list" hx-get="{% url 'communications:message_list_partial' %}" hx-trigger="load">
|
|
<div class="text-center p-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading messages...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div class="text-muted small">
|
|
Showing <span id="showing-count">0</span> of <span id="total-count">0</span> messages
|
|
</div>
|
|
<nav aria-label="Message pagination">
|
|
<ul class="pagination pagination-sm mb-0" id="pagination">
|
|
<!-- Pagination will be loaded dynamically -->
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Message Preview Modal -->
|
|
<div class="modal fade" id="messagePreviewModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Message Preview</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="messagePreviewContent">
|
|
<!-- Content will be loaded dynamically -->
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-primary" onclick="openFullMessage()">Open Full Message</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let selectedMessages = new Set();
|
|
let currentMessageId = null;
|
|
|
|
// Initialize inbox
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadMessages();
|
|
|
|
// Auto-refresh every 30 seconds
|
|
setInterval(function() {
|
|
refreshInbox();
|
|
}, 30000);
|
|
|
|
// Enable keyboard shortcuts
|
|
document.addEventListener('keydown', handleKeyboardShortcuts);
|
|
});
|
|
|
|
function loadMessages(page = 1, filters = {}) {
|
|
const params = new URLSearchParams({
|
|
page: page,
|
|
...filters
|
|
});
|
|
|
|
htmx.ajax('GET', `{% url 'communications:message_list_partial' %}?${params}`, {
|
|
target: '#messages-list',
|
|
swap: 'innerHTML'
|
|
});
|
|
}
|
|
|
|
function refreshInbox() {
|
|
loadMessages();
|
|
updateFolderCounts();
|
|
}
|
|
|
|
function updateFolderCounts() {
|
|
fetch('{% url "communications:folder_counts" %}')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
document.getElementById('inbox-count').textContent = data.inbox || 0;
|
|
// Update other folder counts...
|
|
})
|
|
.catch(error => console.error('Error updating folder counts:', error));
|
|
}
|
|
|
|
function toggleSelectAll() {
|
|
const selectAll = document.getElementById('selectAll');
|
|
const messageCheckboxes = document.querySelectorAll('.message-checkbox');
|
|
|
|
messageCheckboxes.forEach(checkbox => {
|
|
checkbox.checked = selectAll.checked;
|
|
if (selectAll.checked) {
|
|
selectedMessages.add(checkbox.value);
|
|
} else {
|
|
selectedMessages.delete(checkbox.value);
|
|
}
|
|
});
|
|
|
|
updateBulkActionButtons();
|
|
}
|
|
|
|
function toggleMessageSelection(messageId) {
|
|
if (selectedMessages.has(messageId)) {
|
|
selectedMessages.delete(messageId);
|
|
} else {
|
|
selectedMessages.add(messageId);
|
|
}
|
|
|
|
updateBulkActionButtons();
|
|
updateSelectAllState();
|
|
}
|
|
|
|
function updateBulkActionButtons() {
|
|
const hasSelection = selectedMessages.size > 0;
|
|
|
|
document.getElementById('markReadBtn').disabled = !hasSelection;
|
|
document.getElementById('markUnreadBtn').disabled = !hasSelection;
|
|
document.getElementById('starBtn').disabled = !hasSelection;
|
|
document.getElementById('archiveBtn').disabled = !hasSelection;
|
|
document.getElementById('deleteBtn').disabled = !hasSelection;
|
|
}
|
|
|
|
function updateSelectAllState() {
|
|
const selectAll = document.getElementById('selectAll');
|
|
const messageCheckboxes = document.querySelectorAll('.message-checkbox');
|
|
const checkedCount = document.querySelectorAll('.message-checkbox:checked').length;
|
|
|
|
if (checkedCount === 0) {
|
|
selectAll.indeterminate = false;
|
|
selectAll.checked = false;
|
|
} else if (checkedCount === messageCheckboxes.length) {
|
|
selectAll.indeterminate = false;
|
|
selectAll.checked = true;
|
|
} else {
|
|
selectAll.indeterminate = true;
|
|
selectAll.checked = false;
|
|
}
|
|
}
|
|
|
|
function markAsRead() {
|
|
if (selectedMessages.size === 0) return;
|
|
|
|
const messageIds = Array.from(selectedMessages);
|
|
|
|
fetch('{% url "communications:bulk_mark_read" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCsrfToken()
|
|
},
|
|
body: JSON.stringify({ message_ids: messageIds })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
refreshInbox();
|
|
selectedMessages.clear();
|
|
updateBulkActionButtons();
|
|
showToast('Success', `${data.count} messages marked as read`, 'success');
|
|
} else {
|
|
showToast('Error', data.error || 'Failed to mark messages as read', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error marking messages as read:', error);
|
|
showToast('Error', 'Failed to mark messages as read', 'error');
|
|
});
|
|
}
|
|
|
|
function markAsUnread() {
|
|
if (selectedMessages.size === 0) return;
|
|
|
|
const messageIds = Array.from(selectedMessages);
|
|
|
|
fetch('{% url "communications:bulk_mark_unread" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCsrfToken()
|
|
},
|
|
body: JSON.stringify({ message_ids: messageIds })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
refreshInbox();
|
|
selectedMessages.clear();
|
|
updateBulkActionButtons();
|
|
showToast('Success', `${data.count} messages marked as unread`, 'success');
|
|
} else {
|
|
showToast('Error', data.error || 'Failed to mark messages as unread', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error marking messages as unread:', error);
|
|
showToast('Error', 'Failed to mark messages as unread', 'error');
|
|
});
|
|
}
|
|
|
|
function starMessages() {
|
|
if (selectedMessages.size === 0) return;
|
|
|
|
const messageIds = Array.from(selectedMessages);
|
|
|
|
fetch('{% url "communications:bulk_star" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCsrfToken()
|
|
},
|
|
body: JSON.stringify({ message_ids: messageIds })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
refreshInbox();
|
|
selectedMessages.clear();
|
|
updateBulkActionButtons();
|
|
showToast('Success', `${data.count} messages starred`, 'success');
|
|
} else {
|
|
showToast('Error', data.error || 'Failed to star messages', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error starring messages:', error);
|
|
showToast('Error', 'Failed to star messages', 'error');
|
|
});
|
|
}
|
|
|
|
function archiveMessages() {
|
|
if (selectedMessages.size === 0) return;
|
|
|
|
const messageIds = Array.from(selectedMessages);
|
|
|
|
fetch('{% url "communications:bulk_archive" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCsrfToken()
|
|
},
|
|
body: JSON.stringify({ message_ids: messageIds })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
refreshInbox();
|
|
selectedMessages.clear();
|
|
updateBulkActionButtons();
|
|
showToast('Success', `${data.count} messages archived`, 'success');
|
|
} else {
|
|
showToast('Error', data.error || 'Failed to archive messages', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error archiving messages:', error);
|
|
showToast('Error', 'Failed to archive messages', 'error');
|
|
});
|
|
}
|
|
|
|
function deleteMessages() {
|
|
if (selectedMessages.size === 0) return;
|
|
|
|
if (!confirm(`Are you sure you want to delete ${selectedMessages.size} message(s)?`)) {
|
|
return;
|
|
}
|
|
|
|
const messageIds = Array.from(selectedMessages);
|
|
|
|
fetch('{% url "communications:bulk_delete" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCsrfToken()
|
|
},
|
|
body: JSON.stringify({ message_ids: messageIds })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
refreshInbox();
|
|
selectedMessages.clear();
|
|
updateBulkActionButtons();
|
|
showToast('Success', `${data.count} messages deleted`, 'success');
|
|
} else {
|
|
showToast('Error', data.error || 'Failed to delete messages', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error deleting messages:', error);
|
|
showToast('Error', 'Failed to delete messages', 'error');
|
|
});
|
|
}
|
|
|
|
function searchMessages() {
|
|
const query = document.getElementById('searchInput').value;
|
|
loadMessages(1, { search: query });
|
|
}
|
|
|
|
function filterMessages(filter) {
|
|
loadMessages(1, { filter: filter });
|
|
}
|
|
|
|
function clearFilters() {
|
|
document.getElementById('searchInput').value = '';
|
|
loadMessages(1);
|
|
}
|
|
|
|
function sortMessages(sortBy) {
|
|
loadMessages(1, { sort: sortBy });
|
|
}
|
|
|
|
function previewMessage(messageId) {
|
|
currentMessageId = messageId;
|
|
|
|
fetch(`{% url 'communications:message_preview' 'MESSAGE_ID' %}`.replace('MESSAGE_ID', messageId))
|
|
.then(response => response.text())
|
|
.then(html => {
|
|
document.getElementById('messagePreviewContent').innerHTML = html;
|
|
const modal = new bootstrap.Modal(document.getElementById('messagePreviewModal'));
|
|
modal.show();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading message preview:', error);
|
|
showToast('Error', 'Failed to load message preview', 'error');
|
|
});
|
|
}
|
|
|
|
function openFullMessage() {
|
|
if (currentMessageId) {
|
|
window.location.href = `{% url 'communications:message_detail' 'MESSAGE_ID' %}`.replace('MESSAGE_ID', currentMessageId);
|
|
}
|
|
}
|
|
|
|
function handleKeyboardShortcuts(event) {
|
|
// Only handle shortcuts when not in input fields
|
|
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
|
|
return;
|
|
}
|
|
|
|
switch (event.key) {
|
|
case 'c':
|
|
if (event.ctrlKey || event.metaKey) return; // Allow Ctrl+C
|
|
window.location.href = '{% url "communications:message_compose" %}';
|
|
break;
|
|
case 'r':
|
|
refreshInbox();
|
|
break;
|
|
case 'a':
|
|
if (event.ctrlKey || event.metaKey) {
|
|
event.preventDefault();
|
|
document.getElementById('selectAll').click();
|
|
}
|
|
break;
|
|
case 'Delete':
|
|
if (selectedMessages.size > 0) {
|
|
deleteMessages();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
function getCsrfToken() {
|
|
return document.querySelector('[name=csrfmiddlewaretoken]').value;
|
|
}
|
|
|
|
function showToast(title, message, type) {
|
|
// Implementation depends on your toast system
|
|
console.log(`${type.toUpperCase()}: ${title} - ${message}`);
|
|
}
|
|
|
|
// Real-time updates via WebSocket or SSE
|
|
if (typeof EventSource !== "undefined") {
|
|
const eventSource = new EventSource('{% url "communications:inbox_updates" %}');
|
|
|
|
eventSource.onmessage = function(event) {
|
|
const data = JSON.parse(event.data);
|
|
|
|
if (data.type === 'new_message') {
|
|
refreshInbox();
|
|
showToast('New Message', `From: ${data.sender}`, 'info');
|
|
} else if (data.type === 'message_read') {
|
|
// Update message status without full refresh
|
|
updateMessageStatus(data.message_id, 'read');
|
|
}
|
|
};
|
|
}
|
|
|
|
function updateMessageStatus(messageId, status) {
|
|
const messageRow = document.querySelector(`[data-message-id="${messageId}"]`);
|
|
if (messageRow) {
|
|
if (status === 'read') {
|
|
messageRow.classList.remove('fw-bold');
|
|
messageRow.classList.add('text-muted');
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|