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

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