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

1117 lines
46 KiB
HTML

{% extends "base.html" %}
{% load static %}
{% block title %}Communication Log - {{ 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">Communication Log</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">Communication Log</li>
</ol>
</nav>
</div>
<div class="btn-group">
<button type="button" class="btn btn-outline-primary" onclick="exportLog()">
<i class="fas fa-download me-2"></i>Export Log
</button>
<button type="button" class="btn btn-outline-info" onclick="refreshLog()">
<i class="fas fa-sync-alt me-2"></i>Refresh
</button>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-cog me-2"></i>Options
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showAdvancedFilters()">
<i class="fas fa-filter me-2"></i>Advanced Filters
</a></li>
<li><a class="dropdown-item" href="#" onclick="showAnalytics()">
<i class="fas fa-chart-bar me-2"></i>Analytics
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="archiveOldLogs()">
<i class="fas fa-archive me-2"></i>Archive Old Logs
</a></li>
</ul>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4" hx-get="{% url 'communications:log_stats' %}" hx-trigger="load, every 300s">
<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">Today's Communications</div>
<div class="h4 mb-0" id="today-count">{{ stats.today|default:0 }}</div>
</div>
<div class="text-white-50">
<i class="fas fa-comments fa-2x"></i>
</div>
</div>
<div class="mt-2">
<small class="text-white-75">
<i class="fas fa-arrow-up me-1"></i>
{{ stats.today_change|default:0 }}% from yesterday
</small>
</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">Successful Deliveries</div>
<div class="h4 mb-0" id="success-count">{{ stats.successful|default:0 }}</div>
</div>
<div class="text-white-50">
<i class="fas fa-check-circle fa-2x"></i>
</div>
</div>
<div class="mt-2">
<small class="text-white-75">
{{ stats.success_rate|default:0 }}% success rate
</small>
</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">Failed Deliveries</div>
<div class="h4 mb-0" id="failed-count">{{ stats.failed|default:0 }}</div>
</div>
<div class="text-white-50">
<i class="fas fa-exclamation-triangle fa-2x"></i>
</div>
</div>
<div class="mt-2">
<small class="text-white-75">
{{ stats.failure_rate|default:0 }}% failure rate
</small>
</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">Average Response Time</div>
<div class="h4 mb-0" id="response-time">{{ stats.avg_response_time|default:"0s" }}</div>
</div>
<div class="text-white-50">
<i class="fas fa-clock fa-2x"></i>
</div>
</div>
<div class="mt-2">
<small class="text-white-75">
Last 24 hours
</small>
</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">
<!-- Date Range Filter -->
<div class="mb-3">
<label class="form-label fw-bold">Date Range</label>
<select class="form-select" id="dateRangeFilter" onchange="applyFilters()">
<option value="today">Today</option>
<option value="yesterday">Yesterday</option>
<option value="week" selected>This Week</option>
<option value="month">This Month</option>
<option value="quarter">This Quarter</option>
<option value="year">This Year</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="datetime-local" class="form-control form-control-sm" id="dateFrom" onchange="applyFilters()">
</div>
<div class="mb-3">
<label class="form-label small">To</label>
<input type="datetime-local" class="form-control form-control-sm" id="dateTo" onchange="applyFilters()">
</div>
</div>
<!-- Communication Type Filter -->
<div class="mb-3">
<label class="form-label fw-bold">Communication Type</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="typeEmail" value="EMAIL" checked onchange="applyFilters()">
<label class="form-check-label" for="typeEmail">
<i class="fas fa-envelope me-2"></i>Email
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="typeSMS" value="SMS" checked onchange="applyFilters()">
<label class="form-check-label" for="typeSMS">
<i class="fas fa-sms me-2"></i>SMS
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="typePush" value="PUSH" checked onchange="applyFilters()">
<label class="form-check-label" for="typePush">
<i class="fas fa-mobile-alt me-2"></i>Push Notification
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="typeInApp" value="IN_APP" checked onchange="applyFilters()">
<label class="form-check-label" for="typeInApp">
<i class="fas fa-bell me-2"></i>In-App
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="typeMessage" value="MESSAGE" checked onchange="applyFilters()">
<label class="form-check-label" for="typeMessage">
<i class="fas fa-comment me-2"></i>Message
</label>
</div>
</div>
<!-- Status Filter -->
<div class="mb-3">
<label class="form-label fw-bold">Status</label>
<select class="form-select" id="statusFilter" onchange="applyFilters()">
<option value="">All Statuses</option>
<option value="PENDING">Pending</option>
<option value="SENT">Sent</option>
<option value="DELIVERED">Delivered</option>
<option value="READ">Read</option>
<option value="FAILED">Failed</option>
<option value="CANCELLED">Cancelled</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>
<!-- User Filter -->
<div class="mb-3">
<label class="form-label fw-bold">User</label>
<select class="form-select" id="userFilter" onchange="applyFilters()">
<option value="">All Users</option>
<!-- Options will be populated dynamically -->
</select>
</div>
<!-- Department Filter -->
<div class="mb-3">
<label class="form-label fw-bold">Department</label>
<select class="form-select" id="departmentFilter" onchange="applyFilters()">
<option value="">All Departments</option>
<option value="EMERGENCY">Emergency</option>
<option value="CARDIOLOGY">Cardiology</option>
<option value="NEUROLOGY">Neurology</option>
<option value="PEDIATRICS">Pediatrics</option>
<option value="SURGERY">Surgery</option>
<option value="RADIOLOGY">Radiology</option>
<option value="LABORATORY">Laboratory</option>
<option value="PHARMACY">Pharmacy</option>
<option value="ADMINISTRATION">Administration</option>
</select>
</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 Stats -->
<div class="card mt-3">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-chart-pie me-2"></i>Quick Stats
</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="d-flex justify-content-between">
<span class="small">Total Logs:</span>
<span class="badge bg-primary" id="total-logs">0</span>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between">
<span class="small">Success Rate:</span>
<span class="badge bg-success" id="success-rate">0%</span>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between">
<span class="small">Most Active Hour:</span>
<span class="badge bg-info" id="peak-hour">--</span>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between">
<span class="small">Top Channel:</span>
<span class="badge bg-warning" id="top-channel">--</span>
</div>
</div>
</div>
</div>
</div>
<!-- Main Log 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">
<h5 class="mb-0 me-3">Communication Entries</h5>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary" onclick="toggleRealTime()" id="realTimeBtn">
<i class="fas fa-play me-1"></i>Real-time
</button>
<button type="button" class="btn btn-outline-secondary" onclick="toggleAutoRefresh()" id="autoRefreshBtn">
<i class="fas fa-sync-alt me-1"></i>Auto-refresh
</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 logs..." id="searchInput" onkeyup="searchLogs()">
<button class="btn btn-outline-secondary" type="button" onclick="searchLogs()">
<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="sortLogs('timestamp_desc')">Newest First</a></li>
<li><a class="dropdown-item" href="#" onclick="sortLogs('timestamp_asc')">Oldest First</a></li>
<li><a class="dropdown-item" href="#" onclick="sortLogs('status')">By Status</a></li>
<li><a class="dropdown-item" href="#" onclick="sortLogs('type')">By Type</a></li>
<li><a class="dropdown-item" href="#" onclick="sortLogs('priority')">By Priority</a></li>
<li><a class="dropdown-item" href="#" onclick="sortLogs('user')">By User</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Timestamp</th>
<th>Type</th>
<th>User/Recipient</th>
<th>Subject/Content</th>
<th>Status</th>
<th>Priority</th>
<th>Response Time</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="log-entries" hx-get="{% url 'communications:log_entries' %}" hx-trigger="load">
<tr>
<td colspan="8" class="text-center p-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading log entries...</span>
</div>
</td>
</tr>
</tbody>
</table>
</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> entries
</div>
<nav aria-label="Log pagination">
<ul class="pagination pagination-sm mb-0" id="pagination">
<!-- Pagination will be loaded dynamically -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Log Entry Detail Modal -->
<div class="modal fade" id="logDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Communication Log Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="logDetailContent">
<!-- 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-outline-primary" onclick="retryDelivery()">
<i class="fas fa-redo me-2"></i>Retry Delivery
</button>
<button type="button" class="btn btn-outline-info" onclick="viewRelatedLogs()">
<i class="fas fa-link me-2"></i>Related Logs
</button>
</div>
</div>
</div>
</div>
<!-- Advanced Filters Modal -->
<div class="modal fade" id="advancedFiltersModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Advanced Filters</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Response Time Range</label>
<div class="row">
<div class="col-6">
<input type="number" class="form-control" placeholder="Min (ms)" id="minResponseTime">
</div>
<div class="col-6">
<input type="number" class="form-control" placeholder="Max (ms)" id="maxResponseTime">
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Error Code</label>
<input type="text" class="form-control" placeholder="e.g., 404, 500" id="errorCodeFilter">
</div>
<div class="mb-3">
<label class="form-label">IP Address</label>
<input type="text" class="form-control" placeholder="e.g., 192.168.1.1" id="ipAddressFilter">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">User Agent Contains</label>
<input type="text" class="form-control" placeholder="Browser/device info" id="userAgentFilter">
</div>
<div class="mb-3">
<label class="form-label">Content Contains</label>
<input type="text" class="form-control" placeholder="Search in message content" id="contentFilter">
</div>
<div class="mb-3">
<label class="form-label">Delivery Attempts</label>
<select class="form-select" id="deliveryAttemptsFilter">
<option value="">Any</option>
<option value="1">1 attempt</option>
<option value="2">2 attempts</option>
<option value="3">3 attempts</option>
<option value="4+">4+ attempts</option>
</select>
</div>
</div>
</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-outline-warning" onclick="clearAdvancedFilters()">Clear</button>
<button type="button" class="btn btn-primary" onclick="applyAdvancedFilters()">Apply Filters</button>
</div>
</div>
</div>
</div>
<!-- Analytics Modal -->
<div class="modal fade" id="analyticsModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Communication Analytics</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<canvas id="communicationChart" width="400" height="200"></canvas>
</div>
<div class="col-md-6">
<canvas id="statusChart" width="400" height="200"></canvas>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<canvas id="hourlyChart" width="400" height="200"></canvas>
</div>
<div class="col-md-6">
<canvas id="responseTimeChart" width="400" height="200"></canvas>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-outline-primary" onclick="exportAnalytics()">
<i class="fas fa-download me-2"></i>Export Report
</button>
</div>
</div>
</div>
</div>
<script>
let currentLogId = null;
let realTimeEnabled = false;
let autoRefreshEnabled = false;
let autoRefreshInterval = null;
// Initialize communication log page
document.addEventListener('DOMContentLoaded', function() {
loadLogEntries();
loadUserOptions();
setupDateRangeHandler();
// Setup real-time updates
setupRealTimeUpdates();
});
function loadLogEntries(page = 1, filters = {}) {
const params = new URLSearchParams({
page: page,
...filters
});
htmx.ajax('GET', `{% url 'communications:log_entries' %}?${params}`, {
target: '#log-entries',
swap: 'innerHTML'
});
}
function loadUserOptions() {
fetch('{% url "communications:user_options" %}')
.then(response => response.json())
.then(data => {
const userSelect = document.getElementById('userFilter');
data.users.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = user.name;
userSelect.appendChild(option);
});
})
.catch(error => console.error('Error loading user options:', error));
}
function setupDateRangeHandler() {
document.getElementById('dateRangeFilter').addEventListener('change', function() {
const customRange = document.getElementById('customDateRange');
if (this.value === 'custom') {
customRange.style.display = 'block';
} else {
customRange.style.display = 'none';
}
});
}
function applyFilters() {
const filters = {
date_range: document.getElementById('dateRangeFilter').value,
status: document.getElementById('statusFilter').value,
priority: document.getElementById('priorityFilter').value,
user: document.getElementById('userFilter').value,
department: document.getElementById('departmentFilter').value,
search: document.getElementById('searchInput').value
};
// Add communication types
const types = [];
['typeEmail', 'typeSMS', 'typePush', 'typeInApp', 'typeMessage'].forEach(id => {
const checkbox = document.getElementById(id);
if (checkbox.checked) {
types.push(checkbox.value);
}
});
filters.types = types.join(',');
// 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];
});
loadLogEntries(1, filters);
updateQuickStats(filters);
}
function clearFilters() {
document.getElementById('dateRangeFilter').value = 'week';
document.getElementById('statusFilter').value = '';
document.getElementById('priorityFilter').value = '';
document.getElementById('userFilter').value = '';
document.getElementById('departmentFilter').value = '';
document.getElementById('searchInput').value = '';
document.getElementById('customDateRange').style.display = 'none';
// Reset communication type checkboxes
['typeEmail', 'typeSMS', 'typePush', 'typeInApp', 'typeMessage'].forEach(id => {
document.getElementById(id).checked = true;
});
loadLogEntries();
updateQuickStats();
}
function searchLogs() {
applyFilters();
}
function sortLogs(sortBy) {
const currentFilters = getCurrentFilters();
currentFilters.sort = sortBy;
loadLogEntries(1, currentFilters);
}
function getCurrentFilters() {
return {
date_range: document.getElementById('dateRangeFilter').value,
status: document.getElementById('statusFilter').value,
priority: document.getElementById('priorityFilter').value,
user: document.getElementById('userFilter').value,
department: document.getElementById('departmentFilter').value,
search: document.getElementById('searchInput').value
};
}
function refreshLog() {
applyFilters();
updateStats();
}
function updateStats() {
htmx.trigger('[hx-get*="log_stats"]', 'refresh');
}
function updateQuickStats(filters = {}) {
fetch('{% url "communications:log_quick_stats" %}?' + new URLSearchParams(filters))
.then(response => response.json())
.then(data => {
document.getElementById('total-logs').textContent = data.total || 0;
document.getElementById('success-rate').textContent = (data.success_rate || 0) + '%';
document.getElementById('peak-hour').textContent = data.peak_hour || '--';
document.getElementById('top-channel').textContent = data.top_channel || '--';
})
.catch(error => console.error('Error updating quick stats:', error));
}
function viewLogDetail(logId) {
currentLogId = logId;
fetch(`{% url 'communications:log_detail' 'LOG_ID' %}`.replace('LOG_ID', logId))
.then(response => response.text())
.then(html => {
document.getElementById('logDetailContent').innerHTML = html;
const modal = new bootstrap.Modal(document.getElementById('logDetailModal'));
modal.show();
})
.catch(error => {
console.error('Error loading log detail:', error);
showToast('Error', 'Failed to load log details', 'error');
});
}
function retryDelivery() {
if (!currentLogId) return;
if (confirm('Retry delivery for this communication?')) {
fetch(`{% url 'communications:retry_delivery' 'LOG_ID' %}`.replace('LOG_ID', currentLogId), {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken()
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Success', 'Delivery retry initiated', 'success');
bootstrap.Modal.getInstance(document.getElementById('logDetailModal')).hide();
refreshLog();
} else {
showToast('Error', data.error || 'Failed to retry delivery', 'error');
}
})
.catch(error => {
console.error('Error retrying delivery:', error);
showToast('Error', 'Failed to retry delivery', 'error');
});
}
}
function viewRelatedLogs() {
if (!currentLogId) return;
const filters = { related_to: currentLogId };
loadLogEntries(1, filters);
bootstrap.Modal.getInstance(document.getElementById('logDetailModal')).hide();
}
function toggleRealTime() {
realTimeEnabled = !realTimeEnabled;
const btn = document.getElementById('realTimeBtn');
if (realTimeEnabled) {
btn.innerHTML = '<i class="fas fa-pause me-1"></i>Real-time';
btn.classList.remove('btn-outline-primary');
btn.classList.add('btn-primary');
setupRealTimeUpdates();
} else {
btn.innerHTML = '<i class="fas fa-play me-1"></i>Real-time';
btn.classList.remove('btn-primary');
btn.classList.add('btn-outline-primary');
disconnectRealTimeUpdates();
}
}
function toggleAutoRefresh() {
autoRefreshEnabled = !autoRefreshEnabled;
const btn = document.getElementById('autoRefreshBtn');
if (autoRefreshEnabled) {
btn.innerHTML = '<i class="fas fa-stop me-1"></i>Auto-refresh';
btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-secondary');
autoRefreshInterval = setInterval(refreshLog, 30000); // Refresh every 30 seconds
} else {
btn.innerHTML = '<i class="fas fa-sync-alt me-1"></i>Auto-refresh';
btn.classList.remove('btn-secondary');
btn.classList.add('btn-outline-secondary');
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
}
}
}
function setupRealTimeUpdates() {
if (!realTimeEnabled) return;
if (typeof EventSource !== "undefined") {
const eventSource = new EventSource('{% url "communications:log_updates" %}');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'new_log_entry') {
// Add new entry to the top of the log
addNewLogEntry(data.entry);
updateStats();
} else if (data.type === 'status_update') {
// Update existing entry status
updateLogEntryStatus(data.log_id, data.status);
}
};
eventSource.onerror = function(event) {
console.error('Real-time connection error:', event);
realTimeEnabled = false;
toggleRealTime();
};
window.logEventSource = eventSource;
}
}
function disconnectRealTimeUpdates() {
if (window.logEventSource) {
window.logEventSource.close();
window.logEventSource = null;
}
}
function addNewLogEntry(entry) {
const tbody = document.getElementById('log-entries');
const newRow = createLogEntryRow(entry);
tbody.insertBefore(newRow, tbody.firstChild);
// Highlight new entry
newRow.classList.add('table-success');
setTimeout(() => {
newRow.classList.remove('table-success');
}, 3000);
}
function updateLogEntryStatus(logId, status) {
const row = document.querySelector(`[data-log-id="${logId}"]`);
if (row) {
const statusCell = row.querySelector('.status-cell');
if (statusCell) {
statusCell.innerHTML = getStatusBadge(status);
}
}
}
function createLogEntryRow(entry) {
const row = document.createElement('tr');
row.setAttribute('data-log-id', entry.id);
row.innerHTML = `
<td>${new Date(entry.timestamp).toLocaleString()}</td>
<td>${getTypeBadge(entry.type)}</td>
<td>${entry.recipient_name || entry.recipient_email}</td>
<td>${entry.subject || entry.content.substring(0, 50) + '...'}</td>
<td class="status-cell">${getStatusBadge(entry.status)}</td>
<td>${getPriorityBadge(entry.priority)}</td>
<td>${entry.response_time || '--'}</td>
<td>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="viewLogDetail('${entry.id}')">
<i class="fas fa-eye"></i>
</button>
</td>
`;
return row;
}
function getTypeBadge(type) {
const badges = {
'EMAIL': '<span class="badge bg-primary"><i class="fas fa-envelope me-1"></i>Email</span>',
'SMS': '<span class="badge bg-success"><i class="fas fa-sms me-1"></i>SMS</span>',
'PUSH': '<span class="badge bg-info"><i class="fas fa-mobile-alt me-1"></i>Push</span>',
'IN_APP': '<span class="badge bg-warning"><i class="fas fa-bell me-1"></i>In-App</span>',
'MESSAGE': '<span class="badge bg-secondary"><i class="fas fa-comment me-1"></i>Message</span>'
};
return badges[type] || `<span class="badge bg-light text-dark">${type}</span>`;
}
function getStatusBadge(status) {
const badges = {
'PENDING': '<span class="badge bg-warning">Pending</span>',
'SENT': '<span class="badge bg-info">Sent</span>',
'DELIVERED': '<span class="badge bg-success">Delivered</span>',
'READ': '<span class="badge bg-primary">Read</span>',
'FAILED': '<span class="badge bg-danger">Failed</span>',
'CANCELLED': '<span class="badge bg-secondary">Cancelled</span>'
};
return badges[status] || `<span class="badge bg-light text-dark">${status}</span>`;
}
function getPriorityBadge(priority) {
const badges = {
'CRITICAL': '<span class="badge bg-danger">Critical</span>',
'URGENT': '<span class="badge bg-warning">Urgent</span>',
'HIGH': '<span class="badge bg-info">High</span>',
'NORMAL': '<span class="badge bg-secondary">Normal</span>',
'LOW': '<span class="badge bg-light text-dark">Low</span>'
};
return badges[priority] || `<span class="badge bg-light text-dark">${priority}</span>`;
}
function showAdvancedFilters() {
const modal = new bootstrap.Modal(document.getElementById('advancedFiltersModal'));
modal.show();
}
function clearAdvancedFilters() {
document.getElementById('minResponseTime').value = '';
document.getElementById('maxResponseTime').value = '';
document.getElementById('errorCodeFilter').value = '';
document.getElementById('ipAddressFilter').value = '';
document.getElementById('userAgentFilter').value = '';
document.getElementById('contentFilter').value = '';
document.getElementById('deliveryAttemptsFilter').value = '';
}
function applyAdvancedFilters() {
const advancedFilters = {
min_response_time: document.getElementById('minResponseTime').value,
max_response_time: document.getElementById('maxResponseTime').value,
error_code: document.getElementById('errorCodeFilter').value,
ip_address: document.getElementById('ipAddressFilter').value,
user_agent: document.getElementById('userAgentFilter').value,
content_contains: document.getElementById('contentFilter').value,
delivery_attempts: document.getElementById('deliveryAttemptsFilter').value
};
// Combine with current filters
const currentFilters = getCurrentFilters();
const allFilters = { ...currentFilters, ...advancedFilters };
// Remove empty filters
Object.keys(allFilters).forEach(key => {
if (!allFilters[key]) delete allFilters[key];
});
loadLogEntries(1, allFilters);
bootstrap.Modal.getInstance(document.getElementById('advancedFiltersModal')).hide();
}
function showAnalytics() {
const modal = new bootstrap.Modal(document.getElementById('analyticsModal'));
modal.show();
// Load analytics charts
setTimeout(loadAnalyticsCharts, 500); // Wait for modal to be fully shown
}
function loadAnalyticsCharts() {
fetch('{% url "communications:log_analytics" %}')
.then(response => response.json())
.then(data => {
createCommunicationChart(data.communication_data);
createStatusChart(data.status_data);
createHourlyChart(data.hourly_data);
createResponseTimeChart(data.response_time_data);
})
.catch(error => console.error('Error loading analytics:', error));
}
function createCommunicationChart(data) {
const ctx = document.getElementById('communicationChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: 'Communications',
data: data.values,
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Communications Over Time'
}
}
}
});
}
function createStatusChart(data) {
const ctx = document.getElementById('statusChart').getContext('2d');
new Chart(ctx, {
type: 'doughnut',
data: {
labels: data.labels,
datasets: [{
data: data.values,
backgroundColor: [
'#28a745', // Success
'#17a2b8', // Info
'#ffc107', // Warning
'#dc3545', // Danger
'#6c757d' // Secondary
]
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Status Distribution'
}
}
}
});
}
function createHourlyChart(data) {
const ctx = document.getElementById('hourlyChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: data.labels,
datasets: [{
label: 'Communications',
data: data.values,
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Hourly Distribution'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
function createResponseTimeChart(data) {
const ctx = document.getElementById('responseTimeChart').getContext('2d');
new Chart(ctx, {
type: 'scatter',
data: {
datasets: [{
label: 'Response Time',
data: data.values,
backgroundColor: 'rgba(255, 99, 132, 0.5)',
borderColor: 'rgba(255, 99, 132, 1)'
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Response Time Distribution'
}
},
scales: {
x: {
title: {
display: true,
text: 'Time'
}
},
y: {
title: {
display: true,
text: 'Response Time (ms)'
}
}
}
}
});
}
function exportLog() {
const filters = getCurrentFilters();
const params = new URLSearchParams(filters);
window.open(`{% url 'communications:export_log' %}?${params}`, '_blank');
}
function exportAnalytics() {
window.open('{% url "communications:export_analytics" %}', '_blank');
}
function archiveOldLogs() {
if (confirm('Archive logs older than 90 days? This action cannot be undone.')) {
fetch('{% url "communications:archive_old_logs" %}', {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken()
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Success', `${data.archived_count} logs archived`, 'success');
refreshLog();
} else {
showToast('Error', data.error || 'Failed to archive logs', 'error');
}
})
.catch(error => {
console.error('Error archiving logs:', error);
showToast('Error', 'Failed to archive logs', 'error');
});
}
}
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}`);
}
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
disconnectRealTimeUpdates();
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
});
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return;
}
switch (e.key) {
case 'r':
refreshLog();
break;
case 'f':
document.getElementById('searchInput').focus();
break;
case 'a':
showAdvancedFilters();
break;
case 'c':
clearFilters();
break;
case 't':
toggleRealTime();
break;
}
});
</script>
{% endblock %}