2025-08-12 13:33:25 +03:00

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 %}