1117 lines
46 KiB
HTML
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 %}
|
|
|