hospital-management/templates/analytics/report_execution_list.html
2025-08-12 13:33:25 +03:00

609 lines
30 KiB
HTML

{% extends 'base.html' %}
{% load static %}
{% block title %}Report Executions - Hospital Management{% endblock %}
{% block content %}
<div class="content">
<div class="container-fluid">
<!-- Page Header -->
<div class="row">
<div class="col-12">
<div class="page-header">
<div class="page-title">
<h4>Report Executions</h4>
<h6>Monitor and manage report execution history and status</h6>
</div>
<div class="page-btn">
<a href="{% url 'analytics:execute_report' %}" class="btn btn-primary">
<i class="fas fa-play me-1"></i>Execute Report
</a>
</div>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row">
<div class="col-lg-3 col-sm-6 col-12">
<div class="dash-widget">
<div class="dash-widgetimg">
<span><i class="fas fa-file-alt text-primary"></i></span>
</div>
<div class="dash-widgetcontent">
<h5>{{ total_executions|default:"0" }}</h5>
<h6>Total Executions</h6>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="dash-widget">
<div class="dash-widgetimg">
<span><i class="fas fa-check-circle text-success"></i></span>
</div>
<div class="dash-widgetcontent">
<h5>{{ successful_executions|default:"0" }}</h5>
<h6>Successful</h6>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="dash-widget">
<div class="dash-widgetimg">
<span><i class="fas fa-spinner text-warning"></i></span>
</div>
<div class="dash-widgetcontent">
<h5>{{ running_executions|default:"0" }}</h5>
<h6>Running</h6>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="dash-widget">
<div class="dash-widgetimg">
<span><i class="fas fa-exclamation-triangle text-danger"></i></span>
</div>
<div class="dash-widgetcontent">
<h5>{{ failed_executions|default:"0" }}</h5>
<h6>Failed</h6>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-filter me-2"></i>Filters
</h5>
<div class="card-tools">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="clearFilters()">
<i class="fas fa-times me-1"></i>Clear
</button>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="refreshData()">
<i class="fas fa-sync me-1"></i>Refresh
</button>
</div>
</div>
<div class="card-body">
<form method="get" id="filterForm">
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label for="report" class="form-label">Report</label>
<select class="form-select" id="report" name="report">
<option value="">All Reports</option>
{% for report in reports %}
<option value="{{ report.id }}" {% if request.GET.report == report.id|stringformat:"s" %}selected{% endif %}>
{{ report.name }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="">All Status</option>
<option value="pending" {% if request.GET.status == 'pending' %}selected{% endif %}>Pending</option>
<option value="running" {% if request.GET.status == 'running' %}selected{% endif %}>Running</option>
<option value="completed" {% if request.GET.status == 'completed' %}selected{% endif %}>Completed</option>
<option value="failed" {% if request.GET.status == 'failed' %}selected{% endif %}>Failed</option>
<option value="cancelled" {% if request.GET.status == 'cancelled' %}selected{% endif %}>Cancelled</option>
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label for="date_from" class="form-label">From Date</label>
<input type="date" class="form-control" id="date_from" name="date_from"
value="{{ request.GET.date_from }}">
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label for="date_to" class="form-label">To Date</label>
<input type="date" class="form-control" id="date_to" name="date_to"
value="{{ request.GET.date_to }}">
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label for="executed_by" class="form-label">Executed By</label>
<select class="form-select" id="executed_by" name="executed_by">
<option value="">All Users</option>
{% for user in users %}
<option value="{{ user.id }}" {% if request.GET.executed_by == user.id|stringformat:"s" %}selected{% endif %}>
{{ user.get_full_name|default:user.username }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-1">
<div class="form-group">
<label class="form-label">&nbsp;</label>
<button type="submit" class="btn btn-primary d-block">
<i class="fas fa-search"></i>
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Report Executions Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-table me-2"></i>Report Executions
</h5>
<div class="card-tools">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="exportData('pdf')">
<i class="fas fa-file-pdf me-1"></i>PDF
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="exportData('excel')">
<i class="fas fa-file-excel me-1"></i>Excel
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="window.print()">
<i class="fas fa-print me-1"></i>Print
</button>
</div>
</div>
</div>
<div class="card-body">
{% if executions %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>
<a href="?{% url_replace request 'sort' 'report__name' %}">
Report Name
{% if request.GET.sort == 'report__name' %}
<i class="fas fa-sort-up"></i>
{% elif request.GET.sort == '-report__name' %}
<i class="fas fa-sort-down"></i>
{% else %}
<i class="fas fa-sort"></i>
{% endif %}
</a>
</th>
<th>
<a href="?{% url_replace request 'sort' 'started_at' %}">
Started At
{% if request.GET.sort == 'started_at' %}
<i class="fas fa-sort-up"></i>
{% elif request.GET.sort == '-started_at' %}
<i class="fas fa-sort-down"></i>
{% else %}
<i class="fas fa-sort"></i>
{% endif %}
</a>
</th>
<th>Duration</th>
<th>Status</th>
<th>Executed By</th>
<th>Records</th>
<th>File Size</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for execution in executions %}
<tr>
<td>
<div class="report-info">
<strong>{{ execution.report.name }}</strong>
{% if execution.report.description %}
<br><small class="text-muted">{{ execution.report.description|truncatechars:50 }}</small>
{% endif %}
</div>
</td>
<td>
<div class="datetime-info">
{{ execution.started_at|date:"M d, Y" }}
<br><small class="text-muted">{{ execution.started_at|time:"H:i:s" }}</small>
</div>
</td>
<td>
{% if execution.duration %}
<div class="duration-info">
<strong>{{ execution.duration }}</strong>
{% if execution.status == 'running' %}
<br><small class="text-muted">
<i class="fas fa-spinner fa-spin me-1"></i>Running...
</small>
{% endif %}
</div>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>
{% if execution.status == 'completed' %}
<span class="badge bg-success">
<i class="fas fa-check me-1"></i>Completed
</span>
{% elif execution.status == 'running' %}
<span class="badge bg-warning">
<i class="fas fa-spinner fa-spin me-1"></i>Running
</span>
{% elif execution.status == 'failed' %}
<span class="badge bg-danger">
<i class="fas fa-times me-1"></i>Failed
</span>
{% elif execution.status == 'cancelled' %}
<span class="badge bg-secondary">
<i class="fas fa-ban me-1"></i>Cancelled
</span>
{% else %}
<span class="badge bg-info">
<i class="fas fa-clock me-1"></i>Pending
</span>
{% endif %}
</td>
<td>
<div class="user-info">
{{ execution.executed_by.get_full_name|default:execution.executed_by.username }}
<br><small class="text-muted">{{ execution.executed_by.email }}</small>
</div>
</td>
<td>
{% if execution.record_count %}
<strong>{{ execution.record_count|floatformat:0 }}</strong>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>
{% if execution.file_size %}
{{ execution.file_size|filesizeformat }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>
<div class="btn-group" role="group">
<a href="{% url 'analytics:report_execution_detail' execution.pk %}"
class="btn btn-outline-primary btn-sm" title="View Details">
<i class="fas fa-eye"></i>
</a>
{% if execution.status == 'completed' and execution.output_file %}
<button type="button" class="btn btn-outline-success btn-sm"
onclick="downloadReport('{{ execution.id }}')" title="Download">
<i class="fas fa-download"></i>
</button>
{% endif %}
{% if execution.status == 'running' %}
<button type="button" class="btn btn-outline-warning btn-sm"
onclick="cancelExecution('{{ execution.id }}')" title="Cancel">
<i class="fas fa-stop"></i>
</button>
{% endif %}
{% if execution.status == 'failed' %}
<button type="button" class="btn btn-outline-info btn-sm"
onclick="retryExecution('{{ execution.id }}')" title="Retry">
<i class="fas fa-redo"></i>
</button>
{% endif %}
<button type="button" class="btn btn-outline-secondary btn-sm"
onclick="viewLogs('{{ execution.id }}')" title="View Logs">
<i class="fas fa-file-alt"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if is_paginated %}
<div class="d-flex justify-content-between align-items-center mt-3">
<div class="pagination-info">
<span class="text-muted">
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ paginator.count }} entries
</span>
</div>
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm mb-0">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?{% url_replace request 'page' page_obj.previous_page_number %}">
<i class="fas fa-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 class="page-link" href="?{% url_replace request 'page' num %}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?{% url_replace request 'page' page_obj.next_page_number %}">
<i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-file-alt fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Report Executions Found</h5>
<p class="text-muted">No report executions match your current filters. Try adjusting your search criteria or execute a new report.</p>
<a href="{% url 'analytics:execute_report' %}" class="btn btn-primary">
<i class="fas fa-play me-1"></i>Execute Report
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Execution Logs Modal -->
<div class="modal fade" id="executionLogsModal" tabindex="-1" aria-labelledby="executionLogsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="executionLogsModalLabel">
<i class="fas fa-file-alt me-2"></i>Execution Logs
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="executionLogsContent">
<!-- Content will be loaded here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="downloadLogs()">
<i class="fas fa-download me-1"></i>Download Logs
</button>
</div>
</div>
</div>
</div>
<script>
function clearFilters() {
window.location.href = window.location.pathname;
}
function refreshData() {
window.location.reload();
}
function exportData(format) {
const params = new URLSearchParams(window.location.search);
params.set('export', format);
window.location.href = '?' + params.toString();
}
function downloadReport(executionId) {
window.location.href = `/analytics/executions/${executionId}/download/`;
}
function cancelExecution(executionId) {
if (confirm('Are you sure you want to cancel this report execution?')) {
fetch(`/analytics/executions/${executionId}/cancel/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Report execution cancelled successfully.');
window.location.reload();
} else {
alert('Failed to cancel execution: ' + data.error);
}
})
.catch(error => {
alert('Error cancelling execution: ' + error);
});
}
}
function retryExecution(executionId) {
if (confirm('Retry this failed report execution?')) {
fetch(`/analytics/executions/${executionId}/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('Report execution retry initiated.');
window.location.reload();
} else {
alert('Failed to retry execution: ' + data.error);
}
})
.catch(error => {
alert('Error retrying execution: ' + error);
});
}
}
function viewLogs(executionId) {
// Show loading state
document.getElementById('executionLogsContent').innerHTML = `
<div class="text-center py-4">
<i class="fas fa-spinner fa-spin fa-2x text-primary mb-3"></i>
<p>Loading execution logs...</p>
</div>
`;
// Show modal
const modal = new bootstrap.Modal(document.getElementById('executionLogsModal'));
modal.show();
// Load logs
fetch(`/analytics/executions/${executionId}/logs/`)
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('executionLogsContent').innerHTML = `
<div class="logs-container">
<pre class="bg-dark text-light p-3 rounded" style="max-height: 400px; overflow-y: auto;">${data.logs}</pre>
</div>
`;
} else {
document.getElementById('executionLogsContent').innerHTML = `
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
No logs available for this execution.
</div>
`;
}
})
.catch(error => {
document.getElementById('executionLogsContent').innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
Error loading logs: ${error}
</div>
`;
});
}
function downloadLogs() {
alert('Downloading execution logs...');
}
// Auto-refresh for running executions
document.addEventListener('DOMContentLoaded', function() {
const runningExecutions = document.querySelectorAll('.badge.bg-warning');
if (runningExecutions.length > 0) {
// Refresh page every 30 seconds if there are running executions
setTimeout(() => {
window.location.reload();
}, 30000);
}
// Auto-submit form on filter change
const filterInputs = document.querySelectorAll('#filterForm select, #filterForm input');
filterInputs.forEach(input => {
input.addEventListener('change', function() {
if (this.type !== 'date' || (this.value && this.value.trim() !== '')) {
document.getElementById('filterForm').submit();
}
});
});
});
</script>
<style>
.report-info strong {
color: #2c3e50;
}
.datetime-info {
font-size: 0.9rem;
}
.duration-info strong {
color: #0072b5;
}
.user-info {
font-size: 0.9rem;
}
.btn-group .btn {
margin-right: 2px;
}
.btn-group .btn:last-child {
margin-right: 0;
}
.logs-container pre {
font-family: 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.4;
}
@media (max-width: 768px) {
.page-btn {
margin-top: 15px;
}
.card-tools {
margin-top: 15px;
}
.btn-group {
flex-direction: column;
width: 100%;
}
.btn-group .btn {
margin-right: 0;
margin-bottom: 2px;
}
.table-responsive {
font-size: 0.875rem;
}
.pagination-info {
margin-bottom: 15px;
}
}
</style>
{% endblock %}