438 lines
19 KiB
HTML
438 lines
19 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}{{ object.name }} - Analytics Reports{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- BEGIN breadcrumb -->
|
|
<ol class="breadcrumb float-xl-end">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'analytics:dashboard' %}">Analytics</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'analytics:report_list' %}">Reports</a></li>
|
|
<li class="breadcrumb-item active">{{ object.name }}</li>
|
|
</ol>
|
|
<!-- END breadcrumb -->
|
|
|
|
<!-- BEGIN page-header -->
|
|
<h1 class="page-header">
|
|
{{ object.name }}
|
|
<small>{{ object.get_category_display }} Report</small>
|
|
</h1>
|
|
<!-- END page-header -->
|
|
|
|
<div class="row">
|
|
<div class="col-xl-8">
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Report Information</h4>
|
|
<div class="panel-heading-btn">
|
|
<button class="btn btn-xs btn-success me-2" onclick="generateReport()">
|
|
<i class="fa fa-play"></i> Generate
|
|
</button>
|
|
<a href="{% url 'analytics:report_update' object.report_id %}" class="btn btn-xs btn-primary me-2">
|
|
<i class="fa fa-edit"></i> Edit
|
|
</a>
|
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
|
</div>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<table class="table table-borderless">
|
|
<tr>
|
|
<td class="fw-bold" width="150">Name:</td>
|
|
<td>{{ object.name }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Description:</td>
|
|
<td>{{ object.description|default:"No description" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Category:</td>
|
|
<td>
|
|
<span class="badge bg-primary">{{ object.get_category_display }}</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Type:</td>
|
|
<td>
|
|
<i class="fa fa-{% if object.type == 'SCHEDULED' %}clock{% elif object.type == 'ON_DEMAND' %}play{% else %}bolt{% endif %} me-2"></i>
|
|
{{ object.get_type_display }}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Status:</td>
|
|
<td>
|
|
<span class="badge bg-{% if object.status == 'ACTIVE' %}success{% elif object.status == 'DRAFT' %}warning{% else %}secondary{% endif %}">
|
|
{{ object.get_status_display }}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<table class="table table-borderless">
|
|
<tr>
|
|
<td class="fw-bold" width="150">Created:</td>
|
|
<td>{{ object.created_at|date:"M d, Y H:i" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Created By:</td>
|
|
<td>{{ object.created_by.get_full_name|default:"System" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Last Generated:</td>
|
|
<td>
|
|
{% if object.last_generated %}
|
|
{{ object.last_generated|date:"M d, Y H:i" }}
|
|
<br><small class="text-muted">{{ object.last_generated|timesince }} ago</small>
|
|
{% else %}
|
|
<span class="text-muted">Never generated</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Next Run:</td>
|
|
<td>
|
|
{% if object.next_run %}
|
|
{{ object.next_run|date:"M d, Y H:i" }}
|
|
<br><small class="text-muted">{{ object.next_run|timeuntil }} from now</small>
|
|
{% else %}
|
|
<span class="text-muted">Not scheduled</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Format:</td>
|
|
<td>
|
|
{% for format in object.output_formats.all %}
|
|
<span class="badge bg-info me-1">{{ format.name }}</span>
|
|
{% empty %}
|
|
<span class="text-muted">No formats specified</span>
|
|
{% endfor %}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Report Configuration -->
|
|
{% if object.configuration %}
|
|
<div class="mt-4">
|
|
<h6>Report Configuration</h6>
|
|
<div class="bg-light p-3 rounded">
|
|
<pre class="mb-0"><code>{{ object.configuration_formatted }}</code></pre>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- SQL Query -->
|
|
{% if object.sql_query %}
|
|
<div class="mt-4">
|
|
<h6>SQL Query</h6>
|
|
<div class="bg-dark p-3 rounded">
|
|
<pre class="mb-0 text-light"><code>{{ object.sql_query }}</code></pre>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Recent Executions</h4>
|
|
<div class="panel-heading-btn">
|
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
|
</div>
|
|
</div>
|
|
<div class="panel-body">
|
|
{% if recent_executions %}
|
|
<div class="table-responsive">
|
|
<table class="table table-striped">
|
|
<thead>
|
|
<tr>
|
|
<th>Execution Time</th>
|
|
<th>Status</th>
|
|
<th>Duration</th>
|
|
<th>Records</th>
|
|
<th>Size</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for execution in recent_executions %}
|
|
<tr>
|
|
<td>{{ execution.started_at|date:"M d, Y H:i:s" }}</td>
|
|
<td>
|
|
<span class="badge bg-{% if execution.status == 'COMPLETED' %}success{% elif execution.status == 'FAILED' %}danger{% elif execution.status == 'RUNNING' %}warning{% else %}secondary{% endif %}">
|
|
{{ execution.get_status_display }}
|
|
</span>
|
|
</td>
|
|
<td>{{ execution.duration|default:"-" }}</td>
|
|
<td>{{ execution.record_count|default:"-" }}</td>
|
|
<td>{{ execution.file_size_formatted|default:"-" }}</td>
|
|
<td>
|
|
{% if execution.output_file %}
|
|
<a href="{{ execution.output_file.url }}" class="btn btn-xs btn-outline-primary" target="_blank">
|
|
<i class="fa fa-download"></i> Download
|
|
</a>
|
|
{% endif %}
|
|
{% if execution.status == 'FAILED' %}
|
|
<button class="btn btn-xs btn-outline-danger" onclick="viewError({{ execution.id }})">
|
|
<i class="fa fa-exclamation-triangle"></i> Error
|
|
</button>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-4 text-muted">
|
|
<i class="fa fa-history fa-3x mb-3"></i>
|
|
<p>No execution history found for this report.</p>
|
|
<button class="btn btn-primary" onclick="generateReport()">
|
|
<i class="fa fa-play me-2"></i>Generate First Report
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
</div>
|
|
|
|
<div class="col-xl-4">
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Quick Actions</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="d-grid gap-2">
|
|
<button class="btn btn-success" onclick="generateReport()">
|
|
<i class="fa fa-play me-2"></i>Generate Report
|
|
</button>
|
|
|
|
<a href="{% url 'analytics:report_update' object.pk %}" class="btn btn-primary">
|
|
<i class="fa fa-edit me-2"></i>Edit Report
|
|
</a>
|
|
|
|
{% if object.type == 'SCHEDULED' %}
|
|
<button class="btn btn-warning" onclick="scheduleReport()">
|
|
<i class="fa fa-clock me-2"></i>Update Schedule
|
|
</button>
|
|
{% endif %}
|
|
|
|
<button class="btn btn-info" onclick="duplicateReport()">
|
|
<i class="fa fa-copy me-2"></i>Duplicate Report
|
|
</button>
|
|
|
|
<button class="btn btn-outline-secondary" onclick="exportReport()">
|
|
<i class="fa fa-download me-2"></i>Export Definition
|
|
</button>
|
|
|
|
<hr>
|
|
|
|
<a href="{% url 'analytics:report_delete' object.pk %}" class="btn btn-danger">
|
|
<i class="fa fa-trash me-2"></i>Delete Report
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Report Statistics</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="row text-center">
|
|
<div class="col-6 mb-3">
|
|
<div class="fs-24px fw-bold text-primary">{{ object.execution_count }}</div>
|
|
<div class="small text-muted">Total Executions</div>
|
|
</div>
|
|
<div class="col-6 mb-3">
|
|
<div class="fs-24px fw-bold text-success">{{ object.success_rate }}%</div>
|
|
<div class="small text-muted">Success Rate</div>
|
|
</div>
|
|
<div class="col-6 mb-3">
|
|
<div class="fs-24px fw-bold text-info">{{ object.avg_duration }}</div>
|
|
<div class="small text-muted">Avg Duration</div>
|
|
</div>
|
|
<div class="col-6 mb-3">
|
|
<div class="fs-24px fw-bold text-warning">{{ object.avg_records }}</div>
|
|
<div class="small text-muted">Avg Records</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Schedule Information</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
{% if object.schedule %}
|
|
<table class="table table-sm table-borderless">
|
|
<tr>
|
|
<td class="text-muted">Frequency:</td>
|
|
<td>{{ object.schedule.get_frequency_display }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="text-muted">Time:</td>
|
|
<td>{{ object.schedule.time|time:"H:i" }}</td>
|
|
</tr>
|
|
{% if object.schedule.day_of_week %}
|
|
<tr>
|
|
<td class="text-muted">Day of Week:</td>
|
|
<td>{{ object.schedule.get_day_of_week_display }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% if object.schedule.day_of_month %}
|
|
<tr>
|
|
<td class="text-muted">Day of Month:</td>
|
|
<td>{{ object.schedule.day_of_month }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
<tr>
|
|
<td class="text-muted">Active:</td>
|
|
<td>
|
|
<span class="badge bg-{{ object.schedule.is_active|yesno:'success,secondary' }}">
|
|
{{ object.schedule.is_active|yesno:'Yes,No' }}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
{% else %}
|
|
<div class="text-center py-3 text-muted">
|
|
<i class="fa fa-clock fa-2x mb-2"></i>
|
|
<p class="mb-0">No schedule configured.</p>
|
|
{% if object.type == 'SCHEDULED' %}
|
|
<button class="btn btn-sm btn-outline-primary mt-2" onclick="scheduleReport()">
|
|
Add Schedule
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
function generateReport() {
|
|
if (confirm('Generate this report now?')) {
|
|
$.ajax({
|
|
url: '{% url "analytics:execute_report" object.pk %}',
|
|
method: 'POST',
|
|
data: {
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
},
|
|
beforeSend: function() {
|
|
toastr.info('Starting report generation...');
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
toastr.success('Report generation started successfully');
|
|
// Refresh page after a delay to show updated status
|
|
setTimeout(function() {
|
|
location.reload();
|
|
}, 2000);
|
|
} else {
|
|
toastr.error('Failed to generate report: ' + (response.error || 'Unknown error'));
|
|
}
|
|
},
|
|
error: function() {
|
|
toastr.error('An error occurred while generating the report');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function scheduleReport() {
|
|
// Implementation for scheduling
|
|
toastr.info('Schedule management functionality will be implemented');
|
|
}
|
|
|
|
{#function duplicateReport() {#}
|
|
{# if (confirm('Create a copy of this report?')) {#}
|
|
{# $.ajax({#}
|
|
{# url: '{% url "analytics:report_duplicate" object.pk %}',#}
|
|
{# method: 'POST',#}
|
|
{# data: {#}
|
|
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
|
{# },#}
|
|
{# success: function(response) {#}
|
|
{# if (response.success) {#}
|
|
{# toastr.success('Report duplicated successfully');#}
|
|
{# window.location.href = response.redirect_url;#}
|
|
{# } else {#}
|
|
{# toastr.error('Failed to duplicate report');#}
|
|
{# }#}
|
|
{# },#}
|
|
{# error: function() {#}
|
|
{# toastr.error('An error occurred while duplicating the report');#}
|
|
{# }#}
|
|
{# });#}
|
|
{# }#}
|
|
{# }#}
|
|
|
|
{#function exportReport() {#}
|
|
{# window.location.href = '{% url "analytics:report_export_definition" object.pk %}';#}
|
|
{# }#}
|
|
|
|
{#function viewError(executionId) {#}
|
|
{# $.ajax({#}
|
|
{# url: '{% url "analytics:execution_error" 0 %}'.replace('0', executionId),#}
|
|
{# success: function(response) {#}
|
|
{# // Show error details in a modal#}
|
|
{# var modal = $('<div class="modal fade" tabindex="-1">' +#}
|
|
{# '<div class="modal-dialog modal-lg">' +#}
|
|
{# '<div class="modal-content">' +#}
|
|
{# '<div class="modal-header">' +#}
|
|
{# '<h5 class="modal-title">Execution Error</h5>' +#}
|
|
{# '<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +#}
|
|
{# '</div>' +#}
|
|
{# '<div class="modal-body">' +#}
|
|
{# '<pre>' + response.error_message + '</pre>' +#}
|
|
{# '</div>' +#}
|
|
{# '<div class="modal-footer">' +#}
|
|
{# '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>' +#}
|
|
{# '</div>' +#}
|
|
{# '</div>' +#}
|
|
{# '</div>' +#}
|
|
{# '</div>');#}
|
|
{# #}
|
|
{# $('body').append(modal);#}
|
|
{# modal.modal('show');#}
|
|
{# modal.on('hidden.bs.modal', function() {#}
|
|
{# modal.remove();#}
|
|
{# });#}
|
|
{# },#}
|
|
{# error: function() {#}
|
|
{# toastr.error('Failed to load error details');#}
|
|
{# }#}
|
|
{# });#}
|
|
{# }#}
|
|
|
|
// Auto-refresh if report is currently running
|
|
{% if object.is_running %}
|
|
setInterval(function() {
|
|
location.reload();
|
|
}, 10000); // Refresh every 10 seconds
|
|
{% endif %}
|
|
</script>
|
|
{% endblock %}
|
|
|