575 lines
25 KiB
HTML
575 lines
25 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}{% if object %}Edit Metric{% else %}Create Metric{% endif %} - Analytics{% endblock %}
|
|
|
|
{% block css %}
|
|
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
|
<link href="{% static 'assets/plugins/codemirror/lib/codemirror.css' %}" rel="stylesheet" />
|
|
<link href="{% static 'assets/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:metric_list' %}">Metrics</a></li>
|
|
<li class="breadcrumb-item active">{% if object %}Edit Metric{% else %}Create Metric{% endif %}</li>
|
|
</ol>
|
|
<!-- END breadcrumb -->
|
|
|
|
<!-- BEGIN page-header -->
|
|
<h1 class="page-header">
|
|
{% if object %}Edit Metric{% else %}Create New Metric{% endif %}
|
|
<small>{% if object %}{{ object.name }}{% else %}Analytics Metric{% endif %}</small>
|
|
</h1>
|
|
<!-- END page-header -->
|
|
|
|
<form method="post" id="metric-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">Metric 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">Update Frequency <span class="text-danger">*</span></label>
|
|
{{ form.update_frequency }}
|
|
{% if form.update_frequency.errors %}
|
|
<div class="text-danger">{{ form.update_frequency.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Unit</label>
|
|
{{ form.unit }}
|
|
{% if form.unit.errors %}
|
|
<div class="text-danger">{{ form.unit.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">e.g., %, $, patients, hours</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Data Type</label>
|
|
{{ form.data_type }}
|
|
{% if form.data_type.errors %}
|
|
<div class="text-danger">{{ form.data_type.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Target Configuration</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Target Value</label>
|
|
{{ form.target_value }}
|
|
{% if form.target_value.errors %}
|
|
<div class="text-danger">{{ form.target_value.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Optional target value for this metric</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Target Direction</label>
|
|
{{ form.target_direction }}
|
|
{% if form.target_direction.errors %}
|
|
<div class="text-danger">{{ form.target_direction.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Whether higher or lower values are better</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Warning Threshold</label>
|
|
{{ form.warning_threshold }}
|
|
{% if form.warning_threshold.errors %}
|
|
<div class="text-danger">{{ form.warning_threshold.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Value that triggers a warning alert</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Critical Threshold</label>
|
|
{{ form.critical_threshold }}
|
|
{% if form.critical_threshold.errors %}
|
|
<div class="text-danger">{{ form.critical_threshold.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Value that triggers a critical alert</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Calculation 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">SQL query that calculates the metric value. Must return a single numeric value.</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</h6>
|
|
<div id="results-content"></div>
|
|
</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="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Decimal Places</label>
|
|
{{ form.decimal_places }}
|
|
{% if form.decimal_places.errors %}
|
|
<div class="text-danger">{{ form.decimal_places.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Number of decimal places to display</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Chart Type</label>
|
|
{{ form.chart_type }}
|
|
{% if form.chart_type.errors %}
|
|
<div class="text-danger">{{ form.chart_type.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-check mb-3">
|
|
{{ form.is_cumulative }}
|
|
<label class="form-check-label" for="{{ form.is_cumulative.id_for_label }}">
|
|
Cumulative metric
|
|
</label>
|
|
<div class="form-text">Values accumulate over time</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-check mb-3">
|
|
{{ form.show_on_dashboard }}
|
|
<label class="form-check-label" for="{{ form.show_on_dashboard.id_for_label }}">
|
|
Show on dashboard
|
|
</label>
|
|
<div class="form-text">Display this metric on the main dashboard</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Tags</label>
|
|
{{ form.tags }}
|
|
{% if form.tags.errors %}
|
|
<div class="text-danger">{{ form.tags.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Comma-separated tags for categorization</div>
|
|
</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 Metric{% else %}Create Metric{% 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 Calculate
|
|
</button>
|
|
|
|
<a href="{% if object %}{% url 'analytics:metric_detail' object.pk %}{% else %}{% url 'analytics:metric_list' %}{% endif %}" class="btn btn-secondary">
|
|
<i class="fa fa-times me-2"></i>Cancel
|
|
</a>
|
|
|
|
{% if object %}
|
|
<hr>
|
|
<a href="{% url 'analytics:metric_delete' object.pk %}" class="btn btn-danger">
|
|
<i class="fa fa-trash me-2"></i>Delete Metric
|
|
</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_count')">
|
|
Patient Count
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="loadTemplate('revenue')">
|
|
Revenue Metrics
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="loadTemplate('bed_occupancy')">
|
|
Bed Occupancy
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="loadTemplate('staff_utilization')">
|
|
Staff Utilization
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="loadTemplate('wait_time')">
|
|
Average Wait Time
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
|
|
{% if object %}
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Current Status</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<table class="table table-borderless">
|
|
<tr>
|
|
<td class="fw-bold">Current Value:</td>
|
|
<td>
|
|
{% if object.current_value is not None %}
|
|
{{ object.current_value }}{% if object.unit %} {{ object.unit }}{% endif %}
|
|
{% else %}
|
|
<span class="text-muted">No data</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Last Updated:</td>
|
|
<td>{{ object.last_updated|date:"M d, Y H:i"|default:"Never" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Update Count:</td>
|
|
<td>{{ object.update_count }}</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>
|
|
<!-- END panel -->
|
|
{% endif %}
|
|
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Help & Tips</h4>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="small">
|
|
<h6>SQL Query Tips:</h6>
|
|
<ul class="mb-3">
|
|
<li>Query must return a single numeric value</li>
|
|
<li>Use aggregate functions like COUNT, SUM, AVG</li>
|
|
<li>Include appropriate WHERE clauses for filtering</li>
|
|
<li>Test your query before saving</li>
|
|
</ul>
|
|
|
|
<h6>Target Configuration:</h6>
|
|
<ul class="mb-3">
|
|
<li>Set realistic and achievable targets</li>
|
|
<li>Use warning/critical thresholds for alerts</li>
|
|
<li>Consider seasonal variations</li>
|
|
</ul>
|
|
|
|
<h6>Update Frequency:</h6>
|
|
<ul class="mb-0">
|
|
<li><strong>Real-time:</strong> Updates continuously</li>
|
|
<li><strong>Hourly:</strong> Updates every hour</li>
|
|
<li><strong>Daily:</strong> Updates once per day</li>
|
|
<li><strong>Weekly:</strong> Updates weekly</li>
|
|
<li><strong>Monthly:</strong> Updates monthly</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
</div>
|
|
</div>
|
|
</form>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
|
|
<script src="{% static 'assets/plugins/codemirror/lib/codemirror.js' %}"></script>
|
|
<script src="{% static 'assets/plugins/codemirror/mode/sql/sql.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
|
|
});
|
|
|
|
// Form validation
|
|
$('#metric-form').on('submit', function(e) {
|
|
var isValid = true;
|
|
var errors = [];
|
|
|
|
// Required field validation
|
|
if (!$('#id_name').val().trim()) {
|
|
errors.push('Metric name is required');
|
|
isValid = false;
|
|
}
|
|
|
|
if (!sqlEditor.getValue().trim()) {
|
|
errors.push('SQL query is required');
|
|
isValid = false;
|
|
}
|
|
|
|
if (!isValid) {
|
|
e.preventDefault();
|
|
toastr.error('Please correct the following errors:<br>' + errors.join('<br>'));
|
|
}
|
|
});
|
|
|
|
// Update form fields before submission
|
|
$('#metric-form').on('submit', function() {
|
|
$('#id_sql_query').val(sqlEditor.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_metric_query" %}',
|
|
method: 'POST',
|
|
data: {
|
|
'query': query,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
},
|
|
beforeSend: function() {
|
|
toastr.info('Testing query...');
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
$('#results-content').html('<div class="alert alert-success">Query returned: <strong>' + response.value + '</strong></div>');
|
|
$('#query-results').show();
|
|
toastr.success('Query executed successfully. Result: ' + response.value);
|
|
} 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
|
|
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:metric_template" %}',
|
|
data: {
|
|
'template': templateName
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
$('.CodeMirror')[0].CodeMirror.setValue(response.query);
|
|
if (response.name) $('#id_name').val(response.name);
|
|
if (response.description) $('#id_description').val(response.description);
|
|
if (response.unit) $('#id_unit').val(response.unit);
|
|
if (response.category) $('#id_category').val(response.category).trigger('change');
|
|
toastr.success('Template loaded');
|
|
} else {
|
|
toastr.error('Failed to load template');
|
|
}
|
|
},
|
|
error: function() {
|
|
toastr.error('Failed to load template');
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|