521 lines
25 KiB
HTML
521 lines
25 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Notification Center{% endblock %}
|
|
|
|
{% block css %}
|
|
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="content" class="app-content">
|
|
<div class="container">
|
|
<ul class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item active">Notification Center</li>
|
|
</ul>
|
|
|
|
<div class="row align-items-center mb-3">
|
|
<div class="col">
|
|
<h1 class="page-header">Notification Center</h1>
|
|
<p class="text-muted">Manage system notifications and alerts</p>
|
|
</div>
|
|
<div class="col-auto">
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-outline-primary" onclick="markAllAsRead()">
|
|
<i class="fa fa-check me-2"></i>Mark All Read
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="refreshNotifications()">
|
|
<i class="fa fa-refresh me-2"></i>Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notification Summary -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-danger bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-exclamation-circle fa-2x text-danger"></i>
|
|
</div>
|
|
<h5>Unread</h5>
|
|
<div class="fs-24px fw-600 text-danger">{{ unread_count }}</div>
|
|
<div class="text-muted small">{{ total_notifications }} total</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-warning bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-exclamation-triangle fa-2x text-warning"></i>
|
|
</div>
|
|
<h5>High Priority</h5>
|
|
<div class="fs-24px fw-600 text-warning">{{ high_priority_count }}</div>
|
|
<div class="text-muted small">Requires attention</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-info bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-bell fa-2x text-info"></i>
|
|
</div>
|
|
<h5>Today</h5>
|
|
<div class="fs-24px fw-600 text-info">{{ today_count }}</div>
|
|
<div class="text-muted small">New notifications</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-success bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-cog fa-2x text-success"></i>
|
|
</div>
|
|
<h5>System Alerts</h5>
|
|
<div class="fs-24px fw-600 text-success">{{ system_alerts_count }}</div>
|
|
<div class="text-muted small">Active alerts</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notification Filters -->
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<form method="get" class="row g-3 align-items-end">
|
|
<div class="col-md-2">
|
|
<label class="form-label">Type</label>
|
|
<select name="type" class="form-select">
|
|
<option value="">All Types</option>
|
|
<option value="system" {% if request.GET.type == 'system' %}selected{% endif %}>System</option>
|
|
<option value="security" {% if request.GET.type == 'security' %}selected{% endif %}>Security</option>
|
|
<option value="patient" {% if request.GET.type == 'patient' %}selected{% endif %}>Patient</option>
|
|
<option value="appointment" {% if request.GET.type == 'appointment' %}selected{% endif %}>Appointment</option>
|
|
<option value="billing" {% if request.GET.type == 'billing' %}selected{% endif %}>Billing</option>
|
|
<option value="medication" {% if request.GET.type == 'medication' %}selected{% endif %}>Medication</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Priority</label>
|
|
<select name="priority" class="form-select">
|
|
<option value="">All Priorities</option>
|
|
<option value="low" {% if request.GET.priority == 'low' %}selected{% endif %}>Low</option>
|
|
<option value="medium" {% if request.GET.priority == 'medium' %}selected{% endif %}>Medium</option>
|
|
<option value="high" {% if request.GET.priority == 'high' %}selected{% endif %}>High</option>
|
|
<option value="critical" {% if request.GET.priority == 'critical' %}selected{% endif %}>Critical</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Status</label>
|
|
<select name="status" class="form-select">
|
|
<option value="">All Status</option>
|
|
<option value="unread" {% if request.GET.status == 'unread' %}selected{% endif %}>Unread</option>
|
|
<option value="read" {% if request.GET.status == 'read' %}selected{% endif %}>Read</option>
|
|
<option value="archived" {% if request.GET.status == 'archived' %}selected{% endif %}>Archived</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Date From</label>
|
|
<input type="date" name="date_from" class="form-control" value="{{ request.GET.date_from }}">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Date To</label>
|
|
<input type="date" name="date_to" class="form-control" value="{{ request.GET.date_to }}">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fa fa-search me-2"></i>Filter
|
|
</button>
|
|
<a href="{% url 'core:notification_center' %}" class="btn btn-outline-secondary ms-2">
|
|
<i class="fa fa-times"></i>
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notifications List -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h4 class="card-title">Notifications</h4>
|
|
<div class="card-toolbar">
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="selectAll()">
|
|
<i class="fa fa-check-square"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="markSelectedAsRead()">
|
|
<i class="fa fa-eye"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-warning btn-sm" onclick="archiveSelected()">
|
|
<i class="fa fa-archive"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteSelected()">
|
|
<i class="fa fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="notification-list">
|
|
{% for notification in notifications %}
|
|
<div class="notification-item {% if not notification.is_read %}unread{% endif %}" data-id="{{ notification.id }}">
|
|
<div class="d-flex align-items-start">
|
|
<div class="form-check me-3">
|
|
<input class="form-check-input notification-checkbox" type="checkbox" value="{{ notification.id }}">
|
|
</div>
|
|
|
|
<div class="notification-icon me-3">
|
|
<div class="w-40px h-40px bg-{{ notification.priority_color }} bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle">
|
|
<i class="fa fa-{{ notification.type_icon }} text-{{ notification.priority_color }}"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="notification-content flex-1">
|
|
<div class="d-flex align-items-center justify-content-between mb-1">
|
|
<div class="notification-title fw-bold">{{ notification.title }}</div>
|
|
<div class="notification-meta">
|
|
<span class="badge bg-{{ notification.type_color }} me-2">{{ notification.get_type_display }}</span>
|
|
<span class="badge bg-{{ notification.priority_color }}">{{ notification.get_priority_display }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="notification-message text-muted mb-2">
|
|
{{ notification.message|truncatechars:150 }}
|
|
</div>
|
|
|
|
<div class="notification-footer d-flex align-items-center justify-content-between">
|
|
<div class="notification-time text-muted small">
|
|
<i class="fa fa-clock me-1"></i>{{ notification.created_at|timesince }} ago
|
|
{% if notification.sender %}
|
|
<span class="ms-2">by {{ notification.sender.get_full_name }}</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="notification-actions">
|
|
{% if not notification.is_read %}
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="markAsRead('{{ notification.id }}')">
|
|
<i class="fa fa-eye"></i>
|
|
</button>
|
|
{% endif %}
|
|
|
|
{% if notification.action_url %}
|
|
<a href="{{ notification.action_url }}" class="btn btn-outline-success btn-sm">
|
|
<i class="fa fa-external-link"></i>
|
|
</a>
|
|
{% endif %}
|
|
|
|
<button type="button" class="btn btn-outline-info btn-sm" onclick="viewNotificationDetails('{{ notification.id }}')">
|
|
<i class="fa fa-info"></i>
|
|
</button>
|
|
|
|
<button type="button" class="btn btn-outline-warning btn-sm" onclick="archiveNotification('{{ notification.id }}')">
|
|
<i class="fa fa-archive"></i>
|
|
</button>
|
|
|
|
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteNotification('{{ notification.id }}')">
|
|
<i class="fa fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center py-5">
|
|
<i class="fa fa-bell-slash fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No notifications found</h5>
|
|
<p class="text-muted">You're all caught up!</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if is_paginated %}
|
|
<nav aria-label="Notifications pagination" class="mt-4">
|
|
<ul class="pagination justify-content-center">
|
|
{% if page_obj.has_previous %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page=1{% if request.GET.type %}&type={{ request.GET.type }}{% endif %}{% if request.GET.priority %}&priority={{ request.GET.priority }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}{% if request.GET.date_from %}&date_from={{ request.GET.date_from }}{% endif %}{% if request.GET.date_to %}&date_to={{ request.GET.date_to }}{% endif %}">First</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.type %}&type={{ request.GET.type }}{% endif %}{% if request.GET.priority %}&priority={{ request.GET.priority }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}{% if request.GET.date_from %}&date_from={{ request.GET.date_from }}{% endif %}{% if request.GET.date_to %}&date_to={{ request.GET.date_to }}{% endif %}">Previous</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% for num in page_obj.paginator.page_range %}
|
|
{% if page_obj.number == num %}
|
|
<li class="page-item active">
|
|
<span class="page-link">{{ num }}</span>
|
|
</li>
|
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ num }}{% if request.GET.type %}&type={{ request.GET.type }}{% endif %}{% if request.GET.priority %}&priority={{ request.GET.priority }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}{% if request.GET.date_from %}&date_from={{ request.GET.date_from }}{% endif %}{% if request.GET.date_to %}&date_to={{ request.GET.date_to }}{% endif %}">{{ num }}</a>
|
|
</li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if page_obj.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.type %}&type={{ request.GET.type }}{% endif %}{% if request.GET.priority %}&priority={{ request.GET.priority }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}{% if request.GET.date_from %}&date_from={{ request.GET.date_from }}{% endif %}{% if request.GET.date_to %}&date_to={{ request.GET.date_to }}{% endif %}">Next</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.type %}&type={{ request.GET.type }}{% endif %}{% if request.GET.priority %}&priority={{ request.GET.priority }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}{% if request.GET.date_from %}&date_from={{ request.GET.date_from }}{% endif %}{% if request.GET.date_to %}&date_to={{ request.GET.date_to }}{% endif %}">Last</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notification Details Modal -->
|
|
<div class="modal fade" id="notificationDetailsModal" 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="notificationDetailsContent">
|
|
<!-- Content loaded via AJAX -->
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Auto-refresh notifications every 30 seconds
|
|
setInterval(function() {
|
|
updateNotificationCounts();
|
|
}, 30000);
|
|
});
|
|
|
|
function markAsRead(notificationId) {
|
|
$.post('{% url "core:mark_notification_read" %}', {
|
|
'notification_id': notificationId,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
$('[data-id="' + notificationId + '"]').removeClass('unread');
|
|
updateNotificationCounts();
|
|
} else {
|
|
toastr.error('Failed to mark notification as read');
|
|
}
|
|
});
|
|
}
|
|
|
|
function markAllAsRead() {
|
|
if (confirm('Mark all notifications as read?')) {
|
|
$.post('{% url "core:mark_all_notifications_read" %}', {
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
$('.notification-item').removeClass('unread');
|
|
toastr.success('All notifications marked as read');
|
|
updateNotificationCounts();
|
|
} else {
|
|
toastr.error('Failed to mark all notifications as read');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function archiveNotification(notificationId) {
|
|
$.post('{% url "core:archive_notification" %}', {
|
|
'notification_id': notificationId,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
$('[data-id="' + notificationId + '"]').fadeOut();
|
|
toastr.success('Notification archived');
|
|
updateNotificationCounts();
|
|
} else {
|
|
toastr.error('Failed to archive notification');
|
|
}
|
|
});
|
|
}
|
|
|
|
function deleteNotification(notificationId) {
|
|
if (confirm('Delete this notification? This action cannot be undone.')) {
|
|
$.post('{% url "core:delete_notification" %}', {
|
|
'notification_id': notificationId,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
$('[data-id="' + notificationId + '"]').fadeOut();
|
|
toastr.success('Notification deleted');
|
|
updateNotificationCounts();
|
|
} else {
|
|
toastr.error('Failed to delete notification');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function viewNotificationDetails(notificationId) {
|
|
$.get('{% url "core:notification_detail" 0 %}'.replace('0', notificationId), function(data) {
|
|
$('#notificationDetailsContent').html(data);
|
|
$('#notificationDetailsModal').modal('show');
|
|
}).fail(function() {
|
|
toastr.error('Failed to load notification details');
|
|
});
|
|
}
|
|
|
|
function selectAll() {
|
|
$('.notification-checkbox').prop('checked', true);
|
|
}
|
|
|
|
function markSelectedAsRead() {
|
|
var selectedIds = [];
|
|
$('.notification-checkbox:checked').each(function() {
|
|
selectedIds.push($(this).val());
|
|
});
|
|
|
|
if (selectedIds.length === 0) {
|
|
toastr.warning('Please select notifications first');
|
|
return;
|
|
}
|
|
|
|
$.post('{% url "core:bulk_mark_notifications_read" %}', {
|
|
'notification_ids': selectedIds,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
selectedIds.forEach(function(id) {
|
|
$('[data-id="' + id + '"]').removeClass('unread');
|
|
});
|
|
$('.notification-checkbox:checked').prop('checked', false);
|
|
toastr.success('Selected notifications marked as read');
|
|
updateNotificationCounts();
|
|
} else {
|
|
toastr.error('Failed to mark notifications as read');
|
|
}
|
|
});
|
|
}
|
|
|
|
function archiveSelected() {
|
|
var selectedIds = [];
|
|
$('.notification-checkbox:checked').each(function() {
|
|
selectedIds.push($(this).val());
|
|
});
|
|
|
|
if (selectedIds.length === 0) {
|
|
toastr.warning('Please select notifications first');
|
|
return;
|
|
}
|
|
|
|
if (confirm('Archive selected notifications?')) {
|
|
$.post('{% url "core:bulk_archive_notifications" %}', {
|
|
'notification_ids': selectedIds,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
selectedIds.forEach(function(id) {
|
|
$('[data-id="' + id + '"]').fadeOut();
|
|
});
|
|
toastr.success('Selected notifications archived');
|
|
updateNotificationCounts();
|
|
} else {
|
|
toastr.error('Failed to archive notifications');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function deleteSelected() {
|
|
var selectedIds = [];
|
|
$('.notification-checkbox:checked').each(function() {
|
|
selectedIds.push($(this).val());
|
|
});
|
|
|
|
if (selectedIds.length === 0) {
|
|
toastr.warning('Please select notifications first');
|
|
return;
|
|
}
|
|
|
|
if (confirm('Delete selected notifications? This action cannot be undone.')) {
|
|
$.post('{% url "core:bulk_delete_notifications" %}', {
|
|
'notification_ids': selectedIds,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
selectedIds.forEach(function(id) {
|
|
$('[data-id="' + id + '"]').fadeOut();
|
|
});
|
|
toastr.success('Selected notifications deleted');
|
|
updateNotificationCounts();
|
|
} else {
|
|
toastr.error('Failed to delete notifications');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function refreshNotifications() {
|
|
location.reload();
|
|
}
|
|
|
|
function updateNotificationCounts() {
|
|
$.get('{% url "core:notification_counts" %}', function(data) {
|
|
// Update the counts in the summary cards
|
|
$('.unread-count').text(data.unread_count);
|
|
$('.high-priority-count').text(data.high_priority_count);
|
|
$('.today-count').text(data.today_count);
|
|
$('.system-alerts-count').text(data.system_alerts_count);
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.notification-item {
|
|
border-bottom: 1px solid #eee;
|
|
padding: 1rem 0;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.notification-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.notification-item.unread {
|
|
background-color: #f8f9fa;
|
|
border-left: 4px solid #007bff;
|
|
padding-left: 1rem;
|
|
}
|
|
|
|
.notification-item:hover {
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
.notification-actions .btn {
|
|
margin-left: 0.25rem;
|
|
}
|
|
|
|
.notification-title {
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.notification-message {
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.notification-meta {
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|