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

714 lines
36 KiB
HTML

{% extends "base.html" %}
{% load static %}
{% block title %}Delivery Logs - Communications{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Breadcrumb -->
<div class="row">
<div class="col-12">
<div class="page-title-box d-sm-flex align-items-center justify-content-between">
<h4 class="mb-sm-0">Delivery Logs</h4>
<div class="page-title-right">
<ol class="breadcrumb m-0">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'communications:dashboard' %}">Communications</a></li>
<li class="breadcrumb-item active">Delivery Logs</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row">
<div class="col-xl-3 col-md-6">
<div class="card card-h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<span class="text-muted mb-3 lh-1 d-block text-truncate">Total Messages</span>
<h4 class="mb-3">
<span class="counter-value" data-target="{{ total_messages }}">{{ total_messages }}</span>
</h4>
</div>
<div class="flex-shrink-0">
<div class="avatar-sm rounded-circle bg-primary">
<span class="avatar-title rounded-circle bg-primary">
<i class="fas fa-envelope text-white font-size-16"></i>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card card-h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<span class="text-muted mb-3 lh-1 d-block text-truncate">Delivered</span>
<h4 class="mb-3">
<span class="counter-value" data-target="{{ delivered_messages }}">{{ delivered_messages }}</span>
</h4>
</div>
<div class="flex-shrink-0">
<div class="avatar-sm rounded-circle bg-success">
<span class="avatar-title rounded-circle bg-success">
<i class="fas fa-check-circle text-white font-size-16"></i>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card card-h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<span class="text-muted mb-3 lh-1 d-block text-truncate">Failed</span>
<h4 class="mb-3">
<span class="counter-value" data-target="{{ failed_messages }}">{{ failed_messages }}</span>
</h4>
</div>
<div class="flex-shrink-0">
<div class="avatar-sm rounded-circle bg-danger">
<span class="avatar-title rounded-circle bg-danger">
<i class="fas fa-times-circle text-white font-size-16"></i>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card card-h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<span class="text-muted mb-3 lh-1 d-block text-truncate">Success Rate</span>
<h4 class="mb-3">
<span class="counter-value" data-target="{{ success_rate }}">{{ success_rate }}</span>
<small class="text-muted">%</small>
</h4>
</div>
<div class="flex-shrink-0">
<div class="avatar-sm rounded-circle bg-info">
<span class="avatar-title rounded-circle bg-info">
<i class="fas fa-chart-line text-white font-size-16"></i>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Delivery Logs List -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="row align-items-center">
<div class="col">
<h4 class="card-title">Message Delivery Logs</h4>
</div>
<div class="col-auto">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-primary" onclick="retryFailed()">
<i class="fas fa-redo me-1"></i>
Retry Failed
</button>
<button type="button" class="btn btn-outline-secondary" onclick="clearOldLogs()">
<i class="fas fa-trash me-1"></i>
Clear Old Logs
</button>
</div>
</div>
</div>
</div>
<div class="card-body">
<!-- Search and Filters -->
<div class="row mb-3">
<div class="col-md-3">
<div class="search-box">
<div class="position-relative">
<input type="text" class="form-control search"
placeholder="Search messages..."
id="messageSearch">
<i class="bx bx-search-alt search-icon"></i>
</div>
</div>
</div>
<div class="col-md-2">
<select class="form-select" id="statusFilter">
<option value="">All Status</option>
<option value="PENDING">Pending</option>
<option value="DELIVERED">Delivered</option>
<option value="FAILED">Failed</option>
<option value="RETRYING">Retrying</option>
</select>
</div>
<div class="col-md-2">
<select class="form-select" id="channelFilter">
<option value="">All Channels</option>
{% for channel in channels %}
<option value="{{ channel.id }}">{{ channel.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<select class="form-select" id="typeFilter">
<option value="">All Types</option>
<option value="EMAIL">Email</option>
<option value="SMS">SMS</option>
<option value="PUSH">Push</option>
<option value="WEBHOOK">Webhook</option>
<option value="SLACK">Slack</option>
<option value="TEAMS">Teams</option>
</select>
</div>
<div class="col-md-2">
<select class="form-select" id="timeFilter">
<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>
</select>
</div>
<div class="col-md-1">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary" id="refreshLogs">
<i class="fas fa-sync-alt"></i>
</button>
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
<i class="fas fa-download"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="exportLogs('csv')">CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="exportLogs('json')">JSON</a></li>
<li><a class="dropdown-item" href="#" onclick="exportLogs('pdf')">PDF Report</a></li>
</ul>
</div>
</div>
</div>
<!-- View Toggle -->
<div class="row mb-3">
<div class="col-md-6">
<div class="btn-group btn-group-sm" role="group">
<input type="radio" class="btn-check" name="viewMode" id="tableView" checked>
<label class="btn btn-outline-secondary" for="tableView">
<i class="fas fa-table me-1"></i>Table
</label>
<input type="radio" class="btn-check" name="viewMode" id="timelineView">
<label class="btn btn-outline-secondary" for="timelineView">
<i class="fas fa-stream me-1"></i>Timeline
</label>
</div>
</div>
<div class="col-md-6 text-end">
<small class="text-muted">
Auto-refresh: <span id="autoRefreshStatus">ON</span>
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="toggleAutoRefresh()">
<i class="fas fa-pause" id="refreshIcon"></i>
</button>
</small>
</div>
</div>
<!-- Delivery Logs Table -->
<div id="deliveryLogsList">
<div class="table-responsive" id="tableViewContent">
<table class="table table-nowrap table-hover mb-0">
<thead class="table-light">
<tr>
<th scope="col">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="checkAll">
</div>
</th>
<th scope="col">Message</th>
<th scope="col">Channel</th>
<th scope="col">Recipient</th>
<th scope="col">Status</th>
<th scope="col">Sent</th>
<th scope="col">Delivery Time</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for log in object_list %}
<tr class="log-row">
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="{{ log.id }}">
</div>
</td>
<td>
<div class="d-flex align-items-center">
<div class="flex-shrink-0 me-2">
<div class="avatar-xs">
<div class="avatar-title bg-soft-{% if log.channel.channel_type == 'EMAIL' %}primary{% elif log.channel.channel_type == 'SMS' %}success{% elif log.channel.channel_type == 'PUSH' %}warning{% elif log.channel.channel_type == 'WEBHOOK' %}info{% elif log.channel.channel_type == 'SLACK' %}secondary{% else %}dark{% endif %} text-{% if log.channel.channel_type == 'EMAIL' %}primary{% elif log.channel.channel_type == 'SMS' %}success{% elif log.channel.channel_type == 'PUSH' %}warning{% elif log.channel.channel_type == 'WEBHOOK' %}info{% elif log.channel.channel_type == 'SLACK' %}secondary{% else %}dark{% endif %} rounded-circle">
<i class="fas fa-{% if log.channel.channel_type == 'EMAIL' %}envelope{% elif log.channel.channel_type == 'SMS' %}sms{% elif log.channel.channel_type == 'PUSH' %}bell{% elif log.channel.channel_type == 'WEBHOOK' %}link{% elif log.channel.channel_type == 'SLACK' %}slack{% elif log.channel.channel_type == 'TEAMS' %}microsoft{% else %}broadcast-tower{% endif %}"></i>
</div>
</div>
</div>
<div class="flex-grow-1">
<h6 class="mb-0">
<a href="{% url 'communications:delivery_log_detail' log.pk %}" class="text-dark">
{{ log.subject|default:log.content|truncatechars:40 }}
</a>
</h6>
{% if log.message_type %}
<small class="text-muted">{{ log.get_message_type_display }}</small>
{% endif %}
</div>
</div>
</td>
<td>
<a href="{% url 'communications:communication_channel_detail' log.channel.pk %}" class="text-primary">
{{ log.channel.name|truncatechars:20 }}
</a>
<br><small class="text-muted">{{ log.channel.get_channel_type_display }}</small>
</td>
<td>
<div>
{{ log.recipient|truncatechars:25 }}
{% if log.recipient_name %}
<br><small class="text-muted">{{ log.recipient_name|truncatechars:20 }}</small>
{% endif %}
</div>
</td>
<td>
<span class="badge bg-{% if log.status == 'DELIVERED' %}success{% elif log.status == 'FAILED' %}danger{% elif log.status == 'PENDING' %}warning{% elif log.status == 'RETRYING' %}info{% else %}secondary{% endif %}">
{{ log.get_status_display }}
</span>
{% if log.retry_count > 0 %}
<br><small class="text-muted">Retry {{ log.retry_count }}</small>
{% endif %}
</td>
<td>
<span class="text-muted">{{ log.sent_at|timesince }} ago</span>
<br><small class="text-muted">{{ log.sent_at|date:"M d, g:i A" }}</small>
</td>
<td>
{% if log.delivery_time %}
<span class="text-{% if log.delivery_time < 1000 %}success{% elif log.delivery_time < 5000 %}warning{% else %}danger{% endif %}">
{{ log.delivery_time }}ms
</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
<div class="dropdown">
<a href="#" class="dropdown-toggle btn btn-light btn-sm" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</a>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" href="{% url 'communications:delivery_log_detail' log.pk %}">
<i class="fas fa-eye me-2"></i>View Details
</a>
{% if log.status == 'FAILED' %}
<a class="dropdown-item" href="#" onclick="retryMessage({{ log.id }})">
<i class="fas fa-redo me-2"></i>Retry Delivery
</a>
{% endif %}
{% if log.status == 'DELIVERED' and log.channel.channel_type == 'EMAIL' %}
<a class="dropdown-item" href="#" onclick="viewEmailContent({{ log.id }})">
<i class="fas fa-envelope-open me-2"></i>View Email
</a>
{% endif %}
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" onclick="exportLog({{ log.id }})">
<i class="fas fa-download me-2"></i>Export Log
</a>
{% if log.error_message %}
<a class="dropdown-item text-danger" href="#" onclick="viewError({{ log.id }})">
<i class="fas fa-exclamation-triangle me-2"></i>View Error
</a>
{% endif %}
</div>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center py-4">
<div class="d-flex flex-column align-items-center">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No delivery logs found</h5>
<p class="text-muted">Message delivery logs will appear here</p>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Timeline View (Hidden by default) -->
<div id="timelineViewContent" style="display: none;">
<div class="timeline">
{% for log in object_list %}
<div class="timeline-item">
<div class="timeline-marker bg-{% if log.status == 'DELIVERED' %}success{% elif log.status == 'FAILED' %}danger{% elif log.status == 'PENDING' %}warning{% elif log.status == 'RETRYING' %}info{% else %}secondary{% endif %}">
<i class="fas fa-{% if log.channel.channel_type == 'EMAIL' %}envelope{% elif log.channel.channel_type == 'SMS' %}sms{% elif log.channel.channel_type == 'PUSH' %}bell{% elif log.channel.channel_type == 'WEBHOOK' %}link{% elif log.channel.channel_type == 'SLACK' %}slack{% elif log.channel.channel_type == 'TEAMS' %}microsoft{% else %}broadcast-tower{% endif %}"></i>
</div>
<div class="timeline-content">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">{{ log.subject|default:log.content|truncatechars:50 }}</h6>
<p class="text-muted mb-1">
<strong>To:</strong> {{ log.recipient|truncatechars:30 }}<br>
<strong>Via:</strong> {{ log.channel.name }} ({{ log.channel.get_channel_type_display }})
</p>
<small class="text-muted">{{ log.sent_at|date:"M d, Y g:i A" }}</small>
</div>
<div class="text-end">
<span class="badge bg-{% if log.status == 'DELIVERED' %}success{% elif log.status == 'FAILED' %}danger{% elif log.status == 'PENDING' %}warning{% elif log.status == 'RETRYING' %}info{% else %}secondary{% endif %}">
{{ log.get_status_display }}
</span>
{% if log.delivery_time %}
<br><small class="text-muted">{{ log.delivery_time }}ms</small>
{% endif %}
</div>
</div>
{% if log.error_message %}
<div class="mt-2">
<div class="alert alert-danger alert-sm mb-0">
<small><strong>Error:</strong> {{ log.error_message|truncatechars:100 }}</small>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Pagination -->
{% if is_paginated %}
<div class="row">
<div class="col-lg-12">
<ul class="pagination pagination-rounded justify-content-center mt-3 mb-4 pb-1">
{% if page_obj.has_previous %}
<li class="page-item">
<a href="?page={{ page_obj.previous_page_number }}" class="page-link">
<i class="mdi mdi-chevron-left"></i>
</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 href="?page={{ num }}" class="page-link">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a href="?page={{ page_obj.next_page_number }}" class="page-link">
<i class="mdi mdi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block css %}
<style>
.timeline {
position: relative;
padding-left: 30px;
}
.timeline::before {
content: '';
position: absolute;
left: 15px;
top: 0;
bottom: 0;
width: 2px;
background: #dee2e6;
}
.timeline-item {
position: relative;
margin-bottom: 20px;
}
.timeline-marker {
position: absolute;
left: -22px;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
}
.timeline-content {
margin-left: 20px;
}
.alert-sm {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
</style>
{% endblock %}
{% block js %}
<script>
let autoRefresh = true;
let refreshInterval;
// View mode switching
document.querySelectorAll('input[name="viewMode"]').forEach(radio => {
radio.addEventListener('change', function() {
const tableView = document.getElementById('tableViewContent');
const timelineView = document.getElementById('timelineViewContent');
if (this.id === 'tableView') {
tableView.style.display = 'block';
timelineView.style.display = 'none';
} else {
tableView.style.display = 'none';
timelineView.style.display = 'block';
}
});
});
// Search and filter functionality
document.getElementById('messageSearch').addEventListener('input', filterLogs);
document.getElementById('statusFilter').addEventListener('change', filterLogs);
document.getElementById('channelFilter').addEventListener('change', filterLogs);
document.getElementById('typeFilter').addEventListener('change', filterLogs);
document.getElementById('timeFilter').addEventListener('change', filterLogs);
function filterLogs() {
const searchTerm = document.getElementById('messageSearch').value.toLowerCase();
const status = document.getElementById('statusFilter').value;
const channel = document.getElementById('channelFilter').value;
const type = document.getElementById('typeFilter').value;
const timeFilter = document.getElementById('timeFilter').value;
const rows = document.querySelectorAll('.log-row');
rows.forEach(row => {
let show = true;
const text = row.textContent.toLowerCase();
// Search filter
if (searchTerm && !text.includes(searchTerm)) {
show = false;
}
// Status filter
if (status) {
const statusBadge = row.querySelector('td:nth-child(5) .badge');
show = show && statusBadge && statusBadge.textContent.trim().toLowerCase().includes(status.toLowerCase());
}
// Channel filter
if (channel) {
const channelLink = row.querySelector('td:nth-child(3) a');
show = show && channelLink && channelLink.href.includes(`/${channel}/`);
}
// Type filter
if (type) {
const typeText = row.querySelector('td:nth-child(3) small');
show = show && typeText && typeText.textContent.trim().toLowerCase().includes(type.toLowerCase());
}
row.style.display = show ? '' : 'none';
});
}
// Message actions
function retryMessage(logId) {
if (confirm('Retry delivery for this message?')) {
fetch(`/communications/delivery-logs/${logId}/retry/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Message queued for retry');
location.reload();
} else {
alert('Error retrying message: ' + data.error);
}
});
}
}
function retryFailed() {
if (confirm('Retry all failed messages?')) {
fetch('/communications/delivery-logs/retry-failed/', {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`${data.count} messages queued for retry`);
location.reload();
} else {
alert('Error retrying messages: ' + data.error);
}
});
}
}
function clearOldLogs() {
const days = prompt('Clear logs older than how many days?', '30');
if (days && !isNaN(days) && parseInt(days) > 0) {
if (confirm(`Clear all logs older than ${days} days? This cannot be undone.`)) {
fetch('/communications/delivery-logs/clear-old/', {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json',
},
body: JSON.stringify({ days: parseInt(days) })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`${data.count} logs cleared`);
location.reload();
} else {
alert('Error clearing logs: ' + data.error);
}
});
}
}
}
function viewEmailContent(logId) {
window.open(`/communications/delivery-logs/${logId}/email-content/`, '_blank');
}
function viewError(logId) {
fetch(`/communications/delivery-logs/${logId}/error/`)
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`Error Details:\n\n${data.error_message}\n\nStack Trace:\n${data.stack_trace || 'Not available'}`);
} else {
alert('Error retrieving error details');
}
});
}
function exportLog(logId) {
window.open(`/communications/delivery-logs/${logId}/export/`, '_blank');
}
function exportLogs(format) {
const url = `/communications/export/delivery-logs/?format=${format}`;
window.open(url, '_blank');
}
// Auto-refresh functionality
function toggleAutoRefresh() {
autoRefresh = !autoRefresh;
const status = document.getElementById('autoRefreshStatus');
const icon = document.getElementById('refreshIcon');
if (autoRefresh) {
status.textContent = 'ON';
icon.className = 'fas fa-pause';
startAutoRefresh();
} else {
status.textContent = 'OFF';
icon.className = 'fas fa-play';
stopAutoRefresh();
}
}
function startAutoRefresh() {
refreshInterval = setInterval(() => {
if (autoRefresh) {
location.reload();
}
}, 30000); // Refresh every 30 seconds
}
function stopAutoRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
}
}
// Check all functionality
document.getElementById('checkAll').addEventListener('change', function() {
const checkboxes = document.querySelectorAll('tbody input[type="checkbox"]');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
});
// Refresh logs
document.getElementById('refreshLogs').addEventListener('click', function() {
location.reload();
});
// Initialize
document.addEventListener('DOMContentLoaded', function() {
if (autoRefresh) {
startAutoRefresh();
}
});
// Cleanup
window.addEventListener('beforeunload', function() {
stopAutoRefresh();
});
</script>
{% endblock %}