2025-08-12 13:33:25 +03:00

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 %}