760 lines
31 KiB
HTML
760 lines
31 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}Notifications - {{ 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">Notifications</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">Notifications</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-outline-primary" onclick="markAllAsRead()">
|
|
<i class="fas fa-check-double me-2"></i>Mark All Read
|
|
</button>
|
|
<a href="{% url 'communications:notification_settings' %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-cog me-2"></i>Settings
|
|
</a>
|
|
<button type="button" class="btn btn-outline-info" onclick="refreshNotifications()">
|
|
<i class="fas fa-sync-alt me-2"></i>Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notification Stats -->
|
|
<div class="row mb-4" hx-get="{% url 'communications:notification_stats' %}" hx-trigger="load, every 60s">
|
|
<div class="col-xl-3 col-md-6 mb-3">
|
|
<div class="card bg-gradient-primary text-white h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div class="text-white-75 small">Unread</div>
|
|
<div class="h4 mb-0" id="unread-count">{{ stats.unread|default:0 }}</div>
|
|
</div>
|
|
<div class="text-white-50">
|
|
<i class="fas fa-bell fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6 mb-3">
|
|
<div class="card bg-gradient-warning text-white h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div class="text-white-75 small">Urgent</div>
|
|
<div class="h4 mb-0" id="urgent-count">{{ stats.urgent|default:0 }}</div>
|
|
</div>
|
|
<div class="text-white-50">
|
|
<i class="fas fa-exclamation-triangle fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6 mb-3">
|
|
<div class="card bg-gradient-success text-white h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div class="text-white-75 small">Today</div>
|
|
<div class="h4 mb-0" id="today-count">{{ stats.today|default:0 }}</div>
|
|
</div>
|
|
<div class="text-white-50">
|
|
<i class="fas fa-calendar-day fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6 mb-3">
|
|
<div class="card bg-gradient-info text-white h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div class="text-white-75 small">This Week</div>
|
|
<div class="h4 mb-0" id="week-count">{{ stats.week|default:0 }}</div>
|
|
</div>
|
|
<div class="text-white-50">
|
|
<i class="fas fa-calendar-week fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Filters Sidebar -->
|
|
<div class="col-lg-3 mb-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-filter me-2"></i>Filters
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- Status Filter -->
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">Status</label>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="radio" name="statusFilter" id="statusAll" value="all" checked onchange="applyFilters()">
|
|
<label class="form-check-label" for="statusAll">All Notifications</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="radio" name="statusFilter" id="statusUnread" value="unread" onchange="applyFilters()">
|
|
<label class="form-check-label" for="statusUnread">Unread Only</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="radio" name="statusFilter" id="statusRead" value="read" onchange="applyFilters()">
|
|
<label class="form-check-label" for="statusRead">Read Only</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Type Filter -->
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">Type</label>
|
|
<select class="form-select" id="typeFilter" onchange="applyFilters()">
|
|
<option value="">All Types</option>
|
|
<option value="APPOINTMENT">Appointments</option>
|
|
<option value="MEDICATION">Medications</option>
|
|
<option value="LAB_RESULTS">Lab Results</option>
|
|
<option value="BILLING">Billing</option>
|
|
<option value="EMERGENCY">Emergency</option>
|
|
<option value="SYSTEM">System</option>
|
|
<option value="CLINICAL">Clinical</option>
|
|
<option value="ADMINISTRATIVE">Administrative</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Priority Filter -->
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">Priority</label>
|
|
<select class="form-select" id="priorityFilter" onchange="applyFilters()">
|
|
<option value="">All Priorities</option>
|
|
<option value="CRITICAL">Critical</option>
|
|
<option value="URGENT">Urgent</option>
|
|
<option value="HIGH">High</option>
|
|
<option value="NORMAL">Normal</option>
|
|
<option value="LOW">Low</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Date Range Filter -->
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">Date Range</label>
|
|
<select class="form-select" id="dateFilter" onchange="applyFilters()">
|
|
<option value="">All Time</option>
|
|
<option value="today">Today</option>
|
|
<option value="yesterday">Yesterday</option>
|
|
<option value="week">This Week</option>
|
|
<option value="month">This Month</option>
|
|
<option value="custom">Custom Range</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Custom Date Range -->
|
|
<div id="customDateRange" style="display: none;">
|
|
<div class="mb-2">
|
|
<label class="form-label small">From</label>
|
|
<input type="date" class="form-control form-control-sm" id="dateFrom" onchange="applyFilters()">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small">To</label>
|
|
<input type="date" class="form-control form-control-sm" id="dateTo" onchange="applyFilters()">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Clear Filters -->
|
|
<button type="button" class="btn btn-outline-secondary btn-sm w-100" onclick="clearFilters()">
|
|
<i class="fas fa-times me-2"></i>Clear All Filters
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-bolt me-2"></i>Quick Actions
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-grid gap-2">
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="markAllAsRead()">
|
|
<i class="fas fa-check-double me-2"></i>Mark All Read
|
|
</button>
|
|
<button type="button" class="btn btn-outline-warning btn-sm" onclick="snoozeSelected()">
|
|
<i class="fas fa-clock me-2"></i>Snooze Selected
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteSelected()">
|
|
<i class="fas fa-trash me-2"></i>Delete Selected
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info btn-sm" onclick="exportNotifications()">
|
|
<i class="fas fa-download me-2"></i>Export List
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notifications List -->
|
|
<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="markSelectedAsRead()" disabled id="markReadBtn">
|
|
<i class="fas fa-check me-1"></i>Mark Read
|
|
</button>
|
|
<button type="button" class="btn btn-outline-warning" onclick="snoozeSelected()" disabled id="snoozeBtn">
|
|
<i class="fas fa-clock me-1"></i>Snooze
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger" onclick="deleteSelected()" 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 notifications..." id="searchInput" onkeyup="searchNotifications()">
|
|
<button class="btn btn-outline-secondary" type="button" onclick="searchNotifications()">
|
|
<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="sortNotifications('date_desc')">Newest First</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="sortNotifications('date_asc')">Oldest First</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="sortNotifications('priority')">By Priority</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="sortNotifications('type')">By Type</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="sortNotifications('status')">By Status</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div id="notifications-list" hx-get="{% url 'communications:notification_list_partial' %}" hx-trigger="load">
|
|
<div class="text-center p-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading notifications...</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> notifications
|
|
</div>
|
|
<nav aria-label="Notification pagination">
|
|
<ul class="pagination pagination-sm mb-0" id="pagination">
|
|
<!-- Pagination will be loaded dynamically -->
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Snooze Modal -->
|
|
<div class="modal fade" id="snoozeModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Snooze Notifications</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Snooze until:</label>
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<button type="button" class="btn btn-outline-primary w-100 mb-2" onclick="snoozeFor(15)">15 minutes</button>
|
|
<button type="button" class="btn btn-outline-primary w-100 mb-2" onclick="snoozeFor(60)">1 hour</button>
|
|
<button type="button" class="btn btn-outline-primary w-100 mb-2" onclick="snoozeFor(240)">4 hours</button>
|
|
</div>
|
|
<div class="col-6">
|
|
<button type="button" class="btn btn-outline-primary w-100 mb-2" onclick="snoozeFor(480)">8 hours</button>
|
|
<button type="button" class="btn btn-outline-primary w-100 mb-2" onclick="snoozeFor(1440)">1 day</button>
|
|
<button type="button" class="btn btn-outline-primary w-100 mb-2" onclick="snoozeFor(10080)">1 week</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="customSnoozeTime" class="form-label">Custom time:</label>
|
|
<input type="datetime-local" class="form-control" id="customSnoozeTime">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="confirmSnooze()">Snooze</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notification Detail Modal -->
|
|
<div class="modal fade" id="notificationDetailModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Notification Details</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="notificationDetailContent">
|
|
<!-- 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="markCurrentAsRead()">Mark as Read</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let selectedNotifications = new Set();
|
|
let currentNotificationId = null;
|
|
|
|
// Initialize notifications page
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadNotifications();
|
|
|
|
// Auto-refresh every 60 seconds
|
|
setInterval(function() {
|
|
refreshNotifications();
|
|
}, 60000);
|
|
|
|
// Setup date filter change handler
|
|
document.getElementById('dateFilter').addEventListener('change', function() {
|
|
const customRange = document.getElementById('customDateRange');
|
|
if (this.value === 'custom') {
|
|
customRange.style.display = 'block';
|
|
} else {
|
|
customRange.style.display = 'none';
|
|
}
|
|
});
|
|
});
|
|
|
|
function loadNotifications(page = 1, filters = {}) {
|
|
const params = new URLSearchParams({
|
|
page: page,
|
|
...filters
|
|
});
|
|
|
|
htmx.ajax('GET', `{% url 'communications:notification_list_partial' %}?${params}`, {
|
|
target: '#notifications-list',
|
|
swap: 'innerHTML'
|
|
});
|
|
}
|
|
|
|
function refreshNotifications() {
|
|
applyFilters();
|
|
updateStats();
|
|
}
|
|
|
|
function updateStats() {
|
|
htmx.trigger('[hx-get*="notification_stats"]', 'refresh');
|
|
}
|
|
|
|
function applyFilters() {
|
|
const filters = {
|
|
status: document.querySelector('input[name="statusFilter"]:checked').value,
|
|
type: document.getElementById('typeFilter').value,
|
|
priority: document.getElementById('priorityFilter').value,
|
|
date_range: document.getElementById('dateFilter').value,
|
|
search: document.getElementById('searchInput').value
|
|
};
|
|
|
|
// Add custom date range if selected
|
|
if (filters.date_range === 'custom') {
|
|
filters.date_from = document.getElementById('dateFrom').value;
|
|
filters.date_to = document.getElementById('dateTo').value;
|
|
}
|
|
|
|
// Remove empty filters
|
|
Object.keys(filters).forEach(key => {
|
|
if (!filters[key]) delete filters[key];
|
|
});
|
|
|
|
loadNotifications(1, filters);
|
|
}
|
|
|
|
function clearFilters() {
|
|
document.getElementById('statusAll').checked = true;
|
|
document.getElementById('typeFilter').value = '';
|
|
document.getElementById('priorityFilter').value = '';
|
|
document.getElementById('dateFilter').value = '';
|
|
document.getElementById('searchInput').value = '';
|
|
document.getElementById('customDateRange').style.display = 'none';
|
|
|
|
loadNotifications();
|
|
}
|
|
|
|
function searchNotifications() {
|
|
applyFilters();
|
|
}
|
|
|
|
function sortNotifications(sortBy) {
|
|
const currentFilters = getCurrentFilters();
|
|
currentFilters.sort = sortBy;
|
|
loadNotifications(1, currentFilters);
|
|
}
|
|
|
|
function getCurrentFilters() {
|
|
return {
|
|
status: document.querySelector('input[name="statusFilter"]:checked').value,
|
|
type: document.getElementById('typeFilter').value,
|
|
priority: document.getElementById('priorityFilter').value,
|
|
date_range: document.getElementById('dateFilter').value,
|
|
search: document.getElementById('searchInput').value
|
|
};
|
|
}
|
|
|
|
function toggleSelectAll() {
|
|
const selectAll = document.getElementById('selectAll');
|
|
const notificationCheckboxes = document.querySelectorAll('.notification-checkbox');
|
|
|
|
notificationCheckboxes.forEach(checkbox => {
|
|
checkbox.checked = selectAll.checked;
|
|
if (selectAll.checked) {
|
|
selectedNotifications.add(checkbox.value);
|
|
} else {
|
|
selectedNotifications.delete(checkbox.value);
|
|
}
|
|
});
|
|
|
|
updateBulkActionButtons();
|
|
}
|
|
|
|
function toggleNotificationSelection(notificationId) {
|
|
if (selectedNotifications.has(notificationId)) {
|
|
selectedNotifications.delete(notificationId);
|
|
} else {
|
|
selectedNotifications.add(notificationId);
|
|
}
|
|
|
|
updateBulkActionButtons();
|
|
updateSelectAllState();
|
|
}
|
|
|
|
function updateBulkActionButtons() {
|
|
const hasSelection = selectedNotifications.size > 0;
|
|
|
|
document.getElementById('markReadBtn').disabled = !hasSelection;
|
|
document.getElementById('snoozeBtn').disabled = !hasSelection;
|
|
document.getElementById('deleteBtn').disabled = !hasSelection;
|
|
}
|
|
|
|
function updateSelectAllState() {
|
|
const selectAll = document.getElementById('selectAll');
|
|
const notificationCheckboxes = document.querySelectorAll('.notification-checkbox');
|
|
const checkedCount = document.querySelectorAll('.notification-checkbox:checked').length;
|
|
|
|
if (checkedCount === 0) {
|
|
selectAll.indeterminate = false;
|
|
selectAll.checked = false;
|
|
} else if (checkedCount === notificationCheckboxes.length) {
|
|
selectAll.indeterminate = false;
|
|
selectAll.checked = true;
|
|
} else {
|
|
selectAll.indeterminate = true;
|
|
selectAll.checked = false;
|
|
}
|
|
}
|
|
|
|
function markAllAsRead() {
|
|
if (confirm('Mark all notifications as read?')) {
|
|
fetch('{% url "communications:mark_all_notifications_read" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': getCsrfToken()
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
refreshNotifications();
|
|
showToast('Success', `${data.count} notifications marked as read`, 'success');
|
|
} else {
|
|
showToast('Error', data.error || 'Failed to mark notifications as read', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error marking all notifications as read:', error);
|
|
showToast('Error', 'Failed to mark notifications as read', 'error');
|
|
});
|
|
}
|
|
}
|
|
|
|
function markSelectedAsRead() {
|
|
if (selectedNotifications.size === 0) return;
|
|
|
|
const notificationIds = Array.from(selectedNotifications);
|
|
|
|
fetch('{% url "communications:bulk_mark_notifications_read" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCsrfToken()
|
|
},
|
|
body: JSON.stringify({ notification_ids: notificationIds })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
refreshNotifications();
|
|
selectedNotifications.clear();
|
|
updateBulkActionButtons();
|
|
showToast('Success', `${data.count} notifications marked as read`, 'success');
|
|
} else {
|
|
showToast('Error', data.error || 'Failed to mark notifications as read', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error marking notifications as read:', error);
|
|
showToast('Error', 'Failed to mark notifications as read', 'error');
|
|
});
|
|
}
|
|
|
|
function snoozeSelected() {
|
|
if (selectedNotifications.size === 0) return;
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('snoozeModal'));
|
|
modal.show();
|
|
}
|
|
|
|
function snoozeFor(minutes) {
|
|
const snoozeUntil = new Date(Date.now() + minutes * 60000);
|
|
document.getElementById('customSnoozeTime').value = snoozeUntil.toISOString().slice(0, 16);
|
|
confirmSnooze();
|
|
}
|
|
|
|
function confirmSnooze() {
|
|
const snoozeUntil = document.getElementById('customSnoozeTime').value;
|
|
if (!snoozeUntil) {
|
|
alert('Please select a snooze time.');
|
|
return;
|
|
}
|
|
|
|
const notificationIds = Array.from(selectedNotifications);
|
|
|
|
fetch('{% url "communications:snooze_notifications" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCsrfToken()
|
|
},
|
|
body: JSON.stringify({
|
|
notification_ids: notificationIds,
|
|
snooze_until: snoozeUntil
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
refreshNotifications();
|
|
selectedNotifications.clear();
|
|
updateBulkActionButtons();
|
|
bootstrap.Modal.getInstance(document.getElementById('snoozeModal')).hide();
|
|
showToast('Success', `${data.count} notifications snoozed`, 'success');
|
|
} else {
|
|
showToast('Error', data.error || 'Failed to snooze notifications', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error snoozing notifications:', error);
|
|
showToast('Error', 'Failed to snooze notifications', 'error');
|
|
});
|
|
}
|
|
|
|
function deleteSelected() {
|
|
if (selectedNotifications.size === 0) return;
|
|
|
|
if (!confirm(`Are you sure you want to delete ${selectedNotifications.size} notification(s)?`)) {
|
|
return;
|
|
}
|
|
|
|
const notificationIds = Array.from(selectedNotifications);
|
|
|
|
fetch('{% url "communications:bulk_delete_notifications" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCsrfToken()
|
|
},
|
|
body: JSON.stringify({ notification_ids: notificationIds })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
refreshNotifications();
|
|
selectedNotifications.clear();
|
|
updateBulkActionButtons();
|
|
showToast('Success', `${data.count} notifications deleted`, 'success');
|
|
} else {
|
|
showToast('Error', data.error || 'Failed to delete notifications', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error deleting notifications:', error);
|
|
showToast('Error', 'Failed to delete notifications', 'error');
|
|
});
|
|
}
|
|
|
|
function viewNotificationDetail(notificationId) {
|
|
currentNotificationId = notificationId;
|
|
|
|
fetch(`{% url 'communications:notification_detail' 'NOTIFICATION_ID' %}`.replace('NOTIFICATION_ID', notificationId))
|
|
.then(response => response.text())
|
|
.then(html => {
|
|
document.getElementById('notificationDetailContent').innerHTML = html;
|
|
const modal = new bootstrap.Modal(document.getElementById('notificationDetailModal'));
|
|
modal.show();
|
|
|
|
// Mark as read when viewed
|
|
markNotificationAsRead(notificationId);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading notification detail:', error);
|
|
showToast('Error', 'Failed to load notification details', 'error');
|
|
});
|
|
}
|
|
|
|
function markNotificationAsRead(notificationId) {
|
|
fetch(`{% url 'communications:mark_notification_read' 'NOTIFICATION_ID' %}`.replace('NOTIFICATION_ID', notificationId), {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': getCsrfToken()
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Update UI to show as read
|
|
updateNotificationStatus(notificationId, 'read');
|
|
updateStats();
|
|
}
|
|
})
|
|
.catch(error => console.error('Error marking notification as read:', error));
|
|
}
|
|
|
|
function markCurrentAsRead() {
|
|
if (currentNotificationId) {
|
|
markNotificationAsRead(currentNotificationId);
|
|
bootstrap.Modal.getInstance(document.getElementById('notificationDetailModal')).hide();
|
|
}
|
|
}
|
|
|
|
function updateNotificationStatus(notificationId, status) {
|
|
const notificationRow = document.querySelector(`[data-notification-id="${notificationId}"]`);
|
|
if (notificationRow) {
|
|
if (status === 'read') {
|
|
notificationRow.classList.remove('fw-bold');
|
|
notificationRow.classList.add('text-muted');
|
|
const unreadIcon = notificationRow.querySelector('.fa-circle');
|
|
if (unreadIcon) {
|
|
unreadIcon.classList.remove('fa-circle', 'text-primary');
|
|
unreadIcon.classList.add('fa-check-circle', 'text-success');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function exportNotifications() {
|
|
const filters = getCurrentFilters();
|
|
const params = new URLSearchParams(filters);
|
|
|
|
window.open(`{% url 'communications:export_notifications' %}?${params}`, '_blank');
|
|
}
|
|
|
|
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 notifications via SSE
|
|
if (typeof EventSource !== "undefined") {
|
|
const eventSource = new EventSource('{% url "communications:notification_updates" %}');
|
|
|
|
eventSource.onmessage = function(event) {
|
|
const data = JSON.parse(event.data);
|
|
|
|
if (data.type === 'new_notification') {
|
|
refreshNotifications();
|
|
showToast('New Notification', data.message, 'info');
|
|
} else if (data.type === 'notification_read') {
|
|
updateNotificationStatus(data.notification_id, 'read');
|
|
}
|
|
};
|
|
|
|
eventSource.onerror = function(event) {
|
|
console.error('SSE connection error:', event);
|
|
};
|
|
}
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
|
return;
|
|
}
|
|
|
|
switch (e.key) {
|
|
case 'r':
|
|
refreshNotifications();
|
|
break;
|
|
case 'a':
|
|
if (e.ctrlKey || e.metaKey) {
|
|
e.preventDefault();
|
|
document.getElementById('selectAll').click();
|
|
} else {
|
|
markAllAsRead();
|
|
}
|
|
break;
|
|
case 'Delete':
|
|
if (selectedNotifications.size > 0) {
|
|
deleteSelected();
|
|
}
|
|
break;
|
|
case 's':
|
|
if (selectedNotifications.size > 0) {
|
|
snoozeSelected();
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|