562 lines
23 KiB
HTML
562 lines
23 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}{% if object %}Edit Report{% else %}Create Report{% endif %} - Analytics{% endblock %}
|
|
|
|
{% block css %}
|
|
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
|
<link href="{% static 'plugins/codemirror/lib/codemirror.css' %}" rel="stylesheet" />
|
|
<link href="{% static 'plugins/codemirror/theme/material.css' %}" rel="stylesheet" />
|
|
{% 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">{% if object %}Edit Report{% else %}Create Report{% endif %}</li>
|
|
</ol>
|
|
<!-- END breadcrumb -->
|
|
|
|
<!-- BEGIN page-header -->
|
|
<h1 class="page-header">
|
|
{% if object %}Edit Report{% else %}Create New Report{% endif %}
|
|
<small>{% if object %}{{ object.name }}{% else %}Analytics Report{% endif %}</small>
|
|
</h1>
|
|
<!-- END page-header -->
|
|
|
|
<form method="post" id="report-form">
|
|
{% csrf_token %}
|
|
|
|
<div class="row">
|
|
<div class="col-xl-8">
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Basic Information</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="mb-3">
|
|
<label class="form-label">Report Name <span class="text-danger">*</span></label>
|
|
{{ form.name }}
|
|
{% if form.name.errors %}
|
|
<div class="text-danger">{{ form.name.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label">Status <span class="text-danger">*</span></label>
|
|
{{ form.status }}
|
|
{% if form.status.errors %}
|
|
<div class="text-danger">{{ form.status.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Description</label>
|
|
{{ form.description }}
|
|
{% if form.description.errors %}
|
|
<div class="text-danger">{{ form.description.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Category <span class="text-danger">*</span></label>
|
|
{{ form.category }}
|
|
{% if form.category.errors %}
|
|
<div class="text-danger">{{ form.category.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Type <span class="text-danger">*</span></label>
|
|
{{ form.type }}
|
|
{% if form.type.errors %}
|
|
<div class="text-danger">{{ form.type.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Output Formats</label>
|
|
{{ form.output_formats }}
|
|
{% if form.output_formats.errors %}
|
|
<div class="text-danger">{{ form.output_formats.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Select one or more output formats for the report</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Query Configuration</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">SQL Query <span class="text-danger">*</span></label>
|
|
<textarea id="sql-editor" name="sql_query" class="form-control" rows="15">{{ form.sql_query.value|default:"" }}</textarea>
|
|
{% if form.sql_query.errors %}
|
|
<div class="text-danger">{{ form.sql_query.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Write the SQL query that will generate the report data</div>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2 mb-3">
|
|
<button type="button" class="btn btn-outline-primary" onclick="validateQuery()">
|
|
<i class="fa fa-check me-2"></i>Validate Query
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info" onclick="testQuery()">
|
|
<i class="fa fa-play me-2"></i>Test Query
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="formatQuery()">
|
|
<i class="fa fa-code me-2"></i>Format Query
|
|
</button>
|
|
</div>
|
|
|
|
<div id="query-results" class="mt-3" style="display: none;">
|
|
<h6>Query Results Preview</h6>
|
|
<div id="results-content"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse" id="schedule-panel" style="display: none;">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Schedule Configuration</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Frequency</label>
|
|
{{ form.schedule_frequency }}
|
|
{% if form.schedule_frequency.errors %}
|
|
<div class="text-danger">{{ form.schedule_frequency.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Time</label>
|
|
{{ form.schedule_time }}
|
|
{% if form.schedule_time.errors %}
|
|
<div class="text-danger">{{ form.schedule_time.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row" id="weekly-options" style="display: none;">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Day of Week</label>
|
|
{{ form.schedule_day_of_week }}
|
|
{% if form.schedule_day_of_week.errors %}
|
|
<div class="text-danger">{{ form.schedule_day_of_week.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row" id="monthly-options" style="display: none;">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Day of Month</label>
|
|
{{ form.schedule_day_of_month }}
|
|
{% if form.schedule_day_of_month.errors %}
|
|
<div class="text-danger">{{ form.schedule_day_of_month.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-check">
|
|
{{ form.schedule_is_active }}
|
|
<label class="form-check-label" for="{{ form.schedule_is_active.id_for_label }}">
|
|
Schedule is active
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Advanced Configuration</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Configuration (JSON)</label>
|
|
<textarea id="config-editor" name="configuration" class="form-control" rows="10">{{ form.configuration.value|default:"" }}</textarea>
|
|
{% if form.configuration.errors %}
|
|
<div class="text-danger">{{ form.configuration.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Additional configuration options in JSON format</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Timeout (seconds)</label>
|
|
{{ form.timeout }}
|
|
{% if form.timeout.errors %}
|
|
<div class="text-danger">{{ form.timeout.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Max Records</label>
|
|
{{ form.max_records }}
|
|
{% if form.max_records.errors %}
|
|
<div class="text-danger">{{ form.max_records.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-check mb-3">
|
|
{{ form.email_on_completion }}
|
|
<label class="form-check-label" for="{{ form.email_on_completion.id_for_label }}">
|
|
Send email notification on completion
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-check">
|
|
{{ form.email_on_failure }}
|
|
<label class="form-check-label" for="{{ form.email_on_failure.id_for_label }}">
|
|
Send email notification on failure
|
|
</label>
|
|
</div>
|
|
</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">Form Actions</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="d-grid gap-2">
|
|
<button type="submit" class="btn btn-primary btn-lg">
|
|
<i class="fa fa-save me-2"></i>
|
|
{% if object %}Update Report{% else %}Create Report{% endif %}
|
|
</button>
|
|
|
|
{% if object %}
|
|
<button type="submit" name="save_and_continue" class="btn btn-success">
|
|
<i class="fa fa-save me-2"></i>Save & Continue Editing
|
|
</button>
|
|
{% else %}
|
|
<button type="submit" name="save_and_add_another" class="btn btn-info">
|
|
<i class="fa fa-plus me-2"></i>Save & Add Another
|
|
</button>
|
|
{% endif %}
|
|
|
|
<button type="submit" name="save_and_test" class="btn btn-warning">
|
|
<i class="fa fa-play me-2"></i>Save & Test Generate
|
|
</button>
|
|
|
|
<a href="{% if object %}{% url 'analytics:report_detail' object.pk %}{% else %}{% url 'analytics:report_list' %}{% endif %}" class="btn btn-secondary">
|
|
<i class="fa fa-times me-2"></i>Cancel
|
|
</a>
|
|
|
|
{% if object %}
|
|
<hr>
|
|
<a href="{% url 'analytics:report_delete' object.pk %}" class="btn btn-danger">
|
|
<i class="fa fa-trash me-2"></i>Delete Report
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Quick Templates</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="d-grid gap-2">
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="loadTemplate('patient_summary')">
|
|
Patient Summary
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="loadTemplate('financial_report')">
|
|
Financial Report
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="loadTemplate('clinical_metrics')">
|
|
Clinical Metrics
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="loadTemplate('operational_stats')">
|
|
Operational Stats
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
|
|
{% if object %}
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Report Information</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<table class="table table-borderless">
|
|
<tr>
|
|
<td class="fw-bold">Created:</td>
|
|
<td>{{ object.created_at|date:"M d, Y" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Last Modified:</td>
|
|
<td>{{ object.updated_at|date:"M d, Y H:i" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Executions:</td>
|
|
<td>{{ object.execution_count }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Success Rate:</td>
|
|
<td>{{ object.success_rate }}%</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
|
|
<script src="{% static 'plugins/codemirror/lib/codemirror.js' %}"></script>
|
|
<script src="{% static 'plugins/codemirror/mode/sql/sql.js' %}"></script>
|
|
<script src="{% static 'plugins/codemirror/mode/javascript/javascript.js' %}"></script>
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Initialize Select2
|
|
$('.select2').select2({
|
|
theme: 'bootstrap-5',
|
|
width: '100%'
|
|
});
|
|
|
|
// Initialize CodeMirror for SQL
|
|
var sqlEditor = CodeMirror.fromTextArea(document.getElementById('sql-editor'), {
|
|
mode: 'text/x-sql',
|
|
theme: 'material',
|
|
lineNumbers: true,
|
|
autoCloseBrackets: true,
|
|
matchBrackets: true,
|
|
indentWithTabs: true,
|
|
smartIndent: true
|
|
});
|
|
|
|
// Initialize CodeMirror for JSON config
|
|
var configEditor = CodeMirror.fromTextArea(document.getElementById('config-editor'), {
|
|
mode: 'application/json',
|
|
theme: 'material',
|
|
lineNumbers: true,
|
|
autoCloseBrackets: true,
|
|
matchBrackets: true,
|
|
indentWithTabs: false,
|
|
indentUnit: 2
|
|
});
|
|
|
|
// Show/hide schedule panel based on type
|
|
function toggleSchedulePanel() {
|
|
var type = $('#id_type').val();
|
|
if (type === 'SCHEDULED') {
|
|
$('#schedule-panel').show();
|
|
} else {
|
|
$('#schedule-panel').hide();
|
|
}
|
|
}
|
|
|
|
// Show/hide schedule options based on frequency
|
|
function toggleScheduleOptions() {
|
|
var frequency = $('#id_schedule_frequency').val();
|
|
$('#weekly-options, #monthly-options').hide();
|
|
|
|
if (frequency === 'WEEKLY') {
|
|
$('#weekly-options').show();
|
|
} else if (frequency === 'MONTHLY') {
|
|
$('#monthly-options').show();
|
|
}
|
|
}
|
|
|
|
// Event handlers
|
|
$('#id_type').on('change', toggleSchedulePanel);
|
|
$('#id_schedule_frequency').on('change', toggleScheduleOptions);
|
|
|
|
// Initialize visibility
|
|
toggleSchedulePanel();
|
|
toggleScheduleOptions();
|
|
|
|
// Form validation
|
|
$('#report-form').on('submit', function(e) {
|
|
var isValid = true;
|
|
var errors = [];
|
|
|
|
// Required field validation
|
|
if (!$('#id_name').val().trim()) {
|
|
errors.push('Report name is required');
|
|
isValid = false;
|
|
}
|
|
|
|
if (!sqlEditor.getValue().trim()) {
|
|
errors.push('SQL query is required');
|
|
isValid = false;
|
|
}
|
|
|
|
// JSON validation for configuration
|
|
var configValue = configEditor.getValue().trim();
|
|
if (configValue) {
|
|
try {
|
|
JSON.parse(configValue);
|
|
} catch (e) {
|
|
errors.push('Configuration must be valid JSON');
|
|
isValid = false;
|
|
}
|
|
}
|
|
|
|
if (!isValid) {
|
|
e.preventDefault();
|
|
toastr.error('Please correct the following errors:<br>' + errors.join('<br>'));
|
|
}
|
|
});
|
|
|
|
// Update form fields before submission
|
|
$('#report-form').on('submit', function() {
|
|
$('#id_sql_query').val(sqlEditor.getValue());
|
|
$('#id_configuration').val(configEditor.getValue());
|
|
});
|
|
});
|
|
|
|
function validateQuery() {
|
|
var query = $('.CodeMirror')[0].CodeMirror.getValue();
|
|
if (!query.trim()) {
|
|
toastr.warning('Please enter a SQL query first');
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: '{% url "analytics:validate_query" %}',
|
|
method: 'POST',
|
|
data: {
|
|
'query': query,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
},
|
|
success: function(response) {
|
|
if (response.valid) {
|
|
toastr.success('Query is valid');
|
|
} else {
|
|
toastr.error('Query validation failed: ' + response.error);
|
|
}
|
|
},
|
|
error: function() {
|
|
toastr.error('Failed to validate query');
|
|
}
|
|
});
|
|
}
|
|
|
|
function testQuery() {
|
|
var query = $('.CodeMirror')[0].CodeMirror.getValue();
|
|
if (!query.trim()) {
|
|
toastr.warning('Please enter a SQL query first');
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: '{% url "analytics:test_query" %}',
|
|
method: 'POST',
|
|
data: {
|
|
'query': query,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
},
|
|
beforeSend: function() {
|
|
toastr.info('Testing query...');
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
$('#results-content').html(response.html);
|
|
$('#query-results').show();
|
|
toastr.success('Query executed successfully. ' + response.record_count + ' records returned.');
|
|
} else {
|
|
toastr.error('Query test failed: ' + response.error);
|
|
}
|
|
},
|
|
error: function() {
|
|
toastr.error('Failed to test query');
|
|
}
|
|
});
|
|
}
|
|
|
|
function formatQuery() {
|
|
var query = $('.CodeMirror')[0].CodeMirror.getValue();
|
|
if (!query.trim()) {
|
|
toastr.warning('Please enter a SQL query first');
|
|
return;
|
|
}
|
|
|
|
// Simple SQL formatting (basic implementation)
|
|
var formatted = query
|
|
.replace(/\bSELECT\b/gi, '\nSELECT')
|
|
.replace(/\bFROM\b/gi, '\nFROM')
|
|
.replace(/\bWHERE\b/gi, '\nWHERE')
|
|
.replace(/\bGROUP BY\b/gi, '\nGROUP BY')
|
|
.replace(/\bORDER BY\b/gi, '\nORDER BY')
|
|
.replace(/\bHAVING\b/gi, '\nHAVING')
|
|
.replace(/\bJOIN\b/gi, '\nJOIN')
|
|
.replace(/\bLEFT JOIN\b/gi, '\nLEFT JOIN')
|
|
.replace(/\bRIGHT JOIN\b/gi, '\nRIGHT JOIN')
|
|
.replace(/\bINNER JOIN\b/gi, '\nINNER JOIN');
|
|
|
|
$('.CodeMirror')[0].CodeMirror.setValue(formatted);
|
|
toastr.success('Query formatted');
|
|
}
|
|
|
|
function loadTemplate(templateName) {
|
|
$.ajax({
|
|
url: '{% url "analytics:query_template" %}',
|
|
data: {
|
|
'template': templateName
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
$('.CodeMirror')[0].CodeMirror.setValue(response.query);
|
|
toastr.success('Template loaded');
|
|
} else {
|
|
toastr.error('Failed to load template');
|
|
}
|
|
},
|
|
error: function() {
|
|
toastr.error('Failed to load template');
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|