601 lines
27 KiB
HTML
601 lines
27 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Report Execution Details - 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 Execution Details</h4>
|
|
<h6>View detailed information about report execution</h6>
|
|
</div>
|
|
<div class="page-btn">
|
|
<a href="{% url 'analytics:report_execution_list' %}" class="btn btn-outline-secondary me-2">
|
|
<i class="fas fa-arrow-left me-1"></i>Back to List
|
|
</a>
|
|
{% if execution.status == 'completed' and execution.output_file %}
|
|
<a href="{% url 'analytics:download_execution' execution.pk %}" class="btn btn-success">
|
|
<i class="fas fa-download me-1"></i>Download Report
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Execution Status Card -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-info-circle me-2"></i>Execution Status
|
|
</h5>
|
|
<div class="card-tools">
|
|
{% if execution.status == 'running' %}
|
|
<button type="button" class="btn btn-outline-warning btn-sm" onclick="cancelExecution()">
|
|
<i class="fas fa-stop me-1"></i>Cancel
|
|
</button>
|
|
{% elif execution.status == 'failed' %}
|
|
<button type="button" class="btn btn-outline-info btn-sm" onclick="retryExecution()">
|
|
<i class="fas fa-redo me-1"></i>Retry
|
|
</button>
|
|
{% endif %}
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="refreshStatus()">
|
|
<i class="fas fa-sync me-1"></i>Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="execution-status-info">
|
|
<div class="status-badge mb-3">
|
|
{% if execution.status == 'completed' %}
|
|
<span class="badge bg-success fs-6">
|
|
<i class="fas fa-check me-2"></i>Completed Successfully
|
|
</span>
|
|
{% elif execution.status == 'running' %}
|
|
<span class="badge bg-warning fs-6">
|
|
<i class="fas fa-spinner fa-spin me-2"></i>Running
|
|
</span>
|
|
{% elif execution.status == 'failed' %}
|
|
<span class="badge bg-danger fs-6">
|
|
<i class="fas fa-times me-2"></i>Failed
|
|
</span>
|
|
{% elif execution.status == 'cancelled' %}
|
|
<span class="badge bg-secondary fs-6">
|
|
<i class="fas fa-ban me-2"></i>Cancelled
|
|
</span>
|
|
{% else %}
|
|
<span class="badge bg-info fs-6">
|
|
<i class="fas fa-clock me-2"></i>Pending
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if execution.status == 'running' %}
|
|
<div class="progress mb-3">
|
|
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
|
role="progressbar" style="width: {{ execution.progress_percentage|default:0 }}%">
|
|
{{ execution.progress_percentage|default:0 }}%
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<table class="table table-sm">
|
|
<tr>
|
|
<td><strong>Execution ID:</strong></td>
|
|
<td>{{ execution.id }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Started At:</strong></td>
|
|
<td>{{ execution.started_at|date:"M d, Y H:i:s" }}</td>
|
|
</tr>
|
|
{% if execution.completed_at %}
|
|
<tr>
|
|
<td><strong>Completed At:</strong></td>
|
|
<td>{{ execution.completed_at|date:"M d, Y H:i:s" }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
<tr>
|
|
<td><strong>Duration:</strong></td>
|
|
<td>
|
|
{% if execution.duration %}
|
|
{{ execution.duration }}
|
|
{% elif execution.status == 'running' %}
|
|
<span id="runningDuration">Calculating...</span>
|
|
{% else %}
|
|
N/A
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Executed By:</strong></td>
|
|
<td>{{ execution.executed_by.get_full_name|default:execution.executed_by.username }}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="execution-metrics">
|
|
<h6 class="mb-3">Execution Metrics</h6>
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<div class="metric-card text-center p-3 bg-light rounded">
|
|
<div class="metric-value text-primary fs-4 fw-bold">
|
|
{{ execution.record_count|default:"0"|floatformat:0 }}
|
|
</div>
|
|
<div class="metric-label text-muted">Records Processed</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="metric-card text-center p-3 bg-light rounded">
|
|
<div class="metric-value text-success fs-4 fw-bold">
|
|
{{ execution.file_size|filesizeformat|default:"N/A" }}
|
|
</div>
|
|
<div class="metric-label text-muted">Output Size</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if execution.error_message %}
|
|
<div class="alert alert-danger mt-3">
|
|
<h6 class="alert-heading">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>Error Details
|
|
</h6>
|
|
<p class="mb-0">{{ execution.error_message }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Report Information -->
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-file-alt me-2"></i>Report Information
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="report-details">
|
|
<h6 class="text-primary">{{ execution.report.name }}</h6>
|
|
{% if execution.report.description %}
|
|
<p class="text-muted mb-3">{{ execution.report.description }}</p>
|
|
{% endif %}
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<table class="table table-sm">
|
|
<tr>
|
|
<td><strong>Report Type:</strong></td>
|
|
<td>{{ execution.report.get_report_type_display }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Category:</strong></td>
|
|
<td>{{ execution.report.get_category_display }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Output Format:</strong></td>
|
|
<td>{{ execution.output_format|upper }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Data Source:</strong></td>
|
|
<td>{{ execution.report.data_source.name }}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<table class="table table-sm">
|
|
<tr>
|
|
<td><strong>Created By:</strong></td>
|
|
<td>{{ execution.report.created_by.get_full_name|default:execution.report.created_by.username }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Created At:</strong></td>
|
|
<td>{{ execution.report.created_at|date:"M d, Y" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Last Modified:</strong></td>
|
|
<td>{{ execution.report.updated_at|date:"M d, Y" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Is Active:</strong></td>
|
|
<td>
|
|
{% if execution.report.is_active %}
|
|
<span class="badge bg-success">Active</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">Inactive</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-cogs me-2"></i>Execution Parameters
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if execution.parameters %}
|
|
<div class="parameters-list">
|
|
{% for key, value in execution.parameters.items %}
|
|
<div class="parameter-item mb-2">
|
|
<strong>{{ key|title }}:</strong>
|
|
<span class="text-muted">{{ value }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<p class="text-muted">No parameters specified for this execution.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-chart-bar me-2"></i>Quick Actions
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-grid gap-2">
|
|
{% if execution.status == 'completed' %}
|
|
<button type="button" class="btn btn-outline-primary" onclick="previewReport()">
|
|
<i class="fas fa-eye me-2"></i>Preview Report
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info" onclick="shareReport()">
|
|
<i class="fas fa-share me-2"></i>Share Report
|
|
</button>
|
|
{% endif %}
|
|
<button type="button" class="btn btn-outline-secondary" onclick="viewLogs()">
|
|
<i class="fas fa-file-alt me-2"></i>View Execution Logs
|
|
</button>
|
|
<button type="button" class="btn btn-outline-warning" onclick="scheduleReport()">
|
|
<i class="fas fa-calendar me-2"></i>Schedule Report
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Execution Timeline -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-history me-2"></i>Execution Timeline
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="timeline">
|
|
<div class="timeline-item">
|
|
<div class="timeline-marker bg-info"></div>
|
|
<div class="timeline-content">
|
|
<h6 class="timeline-title">Execution Started</h6>
|
|
<p class="timeline-description">Report execution initiated by {{ execution.executed_by.get_full_name|default:execution.executed_by.username }}</p>
|
|
<small class="text-muted">{{ execution.started_at|date:"M d, Y H:i:s" }}</small>
|
|
</div>
|
|
</div>
|
|
|
|
{% if execution.status != 'pending' %}
|
|
<div class="timeline-item">
|
|
<div class="timeline-marker bg-primary"></div>
|
|
<div class="timeline-content">
|
|
<h6 class="timeline-title">Data Processing</h6>
|
|
<p class="timeline-description">Processing data from {{ execution.report.data_source.name }}</p>
|
|
<small class="text-muted">{{ execution.started_at|date:"M d, Y H:i:s" }}</small>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if execution.status == 'completed' %}
|
|
<div class="timeline-item">
|
|
<div class="timeline-marker bg-success"></div>
|
|
<div class="timeline-content">
|
|
<h6 class="timeline-title">Execution Completed</h6>
|
|
<p class="timeline-description">Report generated successfully with {{ execution.record_count|default:"0" }} records</p>
|
|
<small class="text-muted">{{ execution.completed_at|date:"M d, Y H:i:s" }}</small>
|
|
</div>
|
|
</div>
|
|
{% elif execution.status == 'failed' %}
|
|
<div class="timeline-item">
|
|
<div class="timeline-marker bg-danger"></div>
|
|
<div class="timeline-content">
|
|
<h6 class="timeline-title">Execution Failed</h6>
|
|
<p class="timeline-description">{{ execution.error_message|default:"An error occurred during execution" }}</p>
|
|
<small class="text-muted">{{ execution.completed_at|date:"M d, Y H:i:s"|default:"N/A" }}</small>
|
|
</div>
|
|
</div>
|
|
{% elif execution.status == 'cancelled' %}
|
|
<div class="timeline-item">
|
|
<div class="timeline-marker bg-secondary"></div>
|
|
<div class="timeline-content">
|
|
<h6 class="timeline-title">Execution Cancelled</h6>
|
|
<p class="timeline-description">Report execution was cancelled by user</p>
|
|
<small class="text-muted">{{ execution.completed_at|date:"M d, Y H:i:s"|default:"N/A" }}</small>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</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 refreshStatus() {
|
|
window.location.reload();
|
|
}
|
|
|
|
function cancelExecution() {
|
|
if (confirm('Are you sure you want to cancel this report execution?')) {
|
|
fetch(`/analytics/executions/{{ execution.id }}/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() {
|
|
if (confirm('Retry this failed report execution?')) {
|
|
fetch(`/analytics/executions/{{ execution.id }}/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 previewReport() {
|
|
window.open(`/analytics/executions/{{ execution.id }}/preview/`, '_blank');
|
|
}
|
|
|
|
function shareReport() {
|
|
alert('Share report functionality will be implemented.');
|
|
}
|
|
|
|
function viewLogs() {
|
|
// 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/{{ execution.id }}/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 scheduleReport() {
|
|
window.location.href = `/analytics/reports/{{ execution.report.id }}/schedule/`;
|
|
}
|
|
|
|
function downloadLogs() {
|
|
window.location.href = `/analytics/executions/{{ execution.id }}/logs/download/`;
|
|
}
|
|
|
|
// Auto-refresh for running executions
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
{% if execution.status == 'running' %}
|
|
// Update running duration every second
|
|
function updateRunningDuration() {
|
|
const startTime = new Date('{{ execution.started_at|date:"c" }}');
|
|
const now = new Date();
|
|
const duration = Math.floor((now - startTime) / 1000);
|
|
|
|
const hours = Math.floor(duration / 3600);
|
|
const minutes = Math.floor((duration % 3600) / 60);
|
|
const seconds = duration % 60;
|
|
|
|
const durationElement = document.getElementById('runningDuration');
|
|
if (durationElement) {
|
|
durationElement.textContent = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
}
|
|
}
|
|
|
|
// Update immediately and then every second
|
|
updateRunningDuration();
|
|
setInterval(updateRunningDuration, 1000);
|
|
|
|
// Refresh page every 30 seconds for running executions
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 30000);
|
|
{% endif %}
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.execution-status-info .status-badge {
|
|
display: block;
|
|
}
|
|
|
|
.metric-card {
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.timeline {
|
|
position: relative;
|
|
padding-left: 30px;
|
|
}
|
|
|
|
.timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 15px;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background-color: #e9ecef;
|
|
}
|
|
|
|
.timeline-item {
|
|
position: relative;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.timeline-marker {
|
|
position: absolute;
|
|
left: -22px;
|
|
top: 0;
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
border: 3px solid #fff;
|
|
box-shadow: 0 0 0 2px #e9ecef;
|
|
}
|
|
|
|
.timeline-content {
|
|
background-color: #f8f9fa;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
border-left: 4px solid #0072b5;
|
|
}
|
|
|
|
.timeline-title {
|
|
margin-bottom: 5px;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.timeline-description {
|
|
margin-bottom: 5px;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.timeline {
|
|
padding-left: 20px;
|
|
}
|
|
|
|
.timeline-marker {
|
|
left: -17px;
|
|
}
|
|
|
|
.metric-card {
|
|
margin-bottom: 15px;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|