609 lines
30 KiB
HTML
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"> </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 %}
|
|
|