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

734 lines
36 KiB
HTML

{% extends "base.html" %}
{% load static %}
{% block title %}{% if object %}Edit Alert Rule{% else %}Create Alert Rule{% endif %} - Communications{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Breadcrumb -->
<div class="row">
<div class="col-12">
<div class="page-title-box d-sm-flex align-items-center justify-content-between">
<h4 class="mb-sm-0">{% if object %}Edit Alert Rule{% else %}Create Alert Rule{% endif %}</h4>
<div class="page-title-right">
<ol class="breadcrumb m-0">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'communications:dashboard' %}">Communications</a></li>
<li class="breadcrumb-item"><a href="{% url 'communications:alert_rule_list' %}">Alert Rules</a></li>
<li class="breadcrumb-item active">{% if object %}Edit{% else %}Create{% endif %}</li>
</ol>
</div>
</div>
</div>
</div>
<form method="post" id="alertRuleForm">
{% csrf_token %}
<div class="row">
<!-- Main Form -->
<div class="col-lg-8">
<!-- Basic Information -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-exclamation-triangle me-2"></i>
Basic Information
</h5>
</div>
<div class="card-body">
<!-- Rule Name -->
<div class="mb-3">
<label for="{{ form.rule_name.id_for_label }}" class="form-label">Rule Name *</label>
{{ form.rule_name }}
{% if form.rule_name.errors %}
<div class="invalid-feedback d-block">
{{ form.rule_name.errors.0 }}
</div>
{% endif %}
<div class="form-text">Enter a unique, descriptive name for this alert rule</div>
</div>
<!-- Category and Severity -->
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.category.id_for_label }}" class="form-label">Category *</label>
{{ form.category }}
{% if form.category.errors %}
<div class="invalid-feedback d-block">
{{ form.category.errors.0 }}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.severity.id_for_label }}" class="form-label">Severity *</label>
{{ form.severity }}
{% if form.severity.errors %}
<div class="invalid-feedback d-block">
{{ form.severity.errors.0 }}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Description -->
<div class="mb-3">
<label for="{{ form.description.id_for_label }}" class="form-label">Description</label>
{{ form.description }}
{% if form.description.errors %}
<div class="invalid-feedback d-block">
{{ form.description.errors.0 }}
</div>
{% endif %}
<div class="form-text">Describe what this alert rule monitors and when it should trigger</div>
</div>
<!-- Event Type -->
<div class="mb-3">
<label for="{{ form.event_type.id_for_label }}" class="form-label">Event Type</label>
{{ form.event_type }}
{% if form.event_type.errors %}
<div class="invalid-feedback d-block">
{{ form.event_type.errors.0 }}
</div>
{% endif %}
<div class="form-text">Specify the type of event this rule should monitor</div>
</div>
<!-- Active Status -->
<div class="mb-3">
<div class="form-check">
{{ form.is_active }}
<label class="form-check-label" for="{{ form.is_active.id_for_label }}">
Active Rule
</label>
</div>
{% if form.is_active.errors %}
<div class="invalid-feedback d-block">
{{ form.is_active.errors.0 }}
</div>
{% endif %}
<div class="form-text">Only active rules will be evaluated and can trigger alerts</div>
</div>
</div>
</div>
<!-- Condition Configuration -->
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-code me-2"></i>
Condition Configuration
</h5>
<button type="button" class="btn btn-sm btn-outline-info" onclick="showConditionHelp()">
<i class="fas fa-question-circle me-1"></i>Help
</button>
</div>
</div>
<div class="card-body">
<!-- Condition Expression -->
<div class="mb-3">
<label for="{{ form.condition_expression.id_for_label }}" class="form-label">Condition Expression *</label>
{{ form.condition_expression }}
{% if form.condition_expression.errors %}
<div class="invalid-feedback d-block">
{{ form.condition_expression.errors.0 }}
</div>
{% endif %}
<div class="form-text">
<div class="d-flex justify-content-between">
<span>Enter the condition that will trigger this alert (e.g., patient.temperature > 38.5)</span>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="validateExpression()">
<i class="fas fa-check me-1"></i>Validate
</button>
</div>
</div>
</div>
<!-- Condition Parameters -->
<div class="mb-3">
<label for="{{ form.condition_parameters.id_for_label }}" class="form-label">Condition Parameters (JSON)</label>
{{ form.condition_parameters }}
{% if form.condition_parameters.errors %}
<div class="invalid-feedback d-block">
{{ form.condition_parameters.errors.0 }}
</div>
{% endif %}
<div class="form-text">Optional JSON parameters for the condition expression</div>
</div>
<!-- Expression Builder -->
<div class="mb-3">
<label class="form-label">Expression Builder:</label>
<div class="row">
<div class="col-md-4">
<select class="form-select" id="fieldSelector">
<option value="">Select Field...</option>
<option value="patient.temperature">Patient Temperature</option>
<option value="patient.blood_pressure">Blood Pressure</option>
<option value="patient.heart_rate">Heart Rate</option>
<option value="equipment.status">Equipment Status</option>
<option value="medication.stock">Medication Stock</option>
<option value="appointment.status">Appointment Status</option>
<option value="system.cpu_usage">CPU Usage</option>
<option value="system.memory_usage">Memory Usage</option>
</select>
</div>
<div class="col-md-3">
<select class="form-select" id="operatorSelector">
<option value="">Select Operator...</option>
<option value=">">&gt; (greater than)</option>
<option value="<">&lt; (less than)</option>
<option value=">=">&gt;= (greater or equal)</option>
<option value="<=">&lt;= (less or equal)</option>
<option value="==">=== (equals)</option>
<option value="!=">&ne; (not equals)</option>
<option value="contains">contains</option>
<option value="in">in</option>
</select>
</div>
<div class="col-md-3">
<input type="text" class="form-control" id="valueInput" placeholder="Value">
</div>
<div class="col-md-2">
<button type="button" class="btn btn-outline-primary" onclick="addToExpression()">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Notification Settings -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-bell me-2"></i>
Notification Settings
</h5>
</div>
<div class="card-body">
<!-- Notification Template -->
<div class="mb-3">
<label for="{{ form.notification_template.id_for_label }}" class="form-label">Notification Template</label>
<div class="input-group">
{{ form.notification_template }}
<button class="btn btn-outline-secondary" type="button" onclick="previewTemplate()">
<i class="fas fa-eye me-1"></i>Preview
</button>
</div>
{% if form.notification_template.errors %}
<div class="invalid-feedback d-block">
{{ form.notification_template.errors.0 }}
</div>
{% endif %}
<div class="form-text">Select a template for alert notifications (optional)</div>
</div>
<!-- Cooldown and Limits -->
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.cooldown_minutes.id_for_label }}" class="form-label">Cooldown Period (minutes)</label>
{{ form.cooldown_minutes }}
{% if form.cooldown_minutes.errors %}
<div class="invalid-feedback d-block">
{{ form.cooldown_minutes.errors.0 }}
</div>
{% endif %}
<div class="form-text">Minimum time between alerts</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.max_triggers_per_day.id_for_label }}" class="form-label">Max Triggers/Day</label>
{{ form.max_triggers_per_day }}
{% if form.max_triggers_per_day.errors %}
<div class="invalid-feedback d-block">
{{ form.max_triggers_per_day.errors.0 }}
</div>
{% endif %}
<div class="form-text">Maximum alerts per day (0 = unlimited)</div>
</div>
</div>
</div>
<!-- Advanced Options -->
<div class="row">
<div class="col-md-4">
<div class="form-check mb-3">
{{ form.auto_resolve }}
<label class="form-check-label" for="{{ form.auto_resolve.id_for_label }}">
Auto Resolve
</label>
<div class="form-text">Automatically resolve when condition is no longer met</div>
</div>
</div>
<div class="col-md-4">
<div class="form-check mb-3">
{{ form.escalation_enabled }}
<label class="form-check-label" for="{{ form.escalation_enabled.id_for_label }}">
Enable Escalation
</label>
<div class="form-text">Escalate unacknowledged alerts</div>
</div>
</div>
<div class="col-md-4">
<div class="form-check mb-3">
{{ form.snooze_enabled }}
<label class="form-check-label" for="{{ form.snooze_enabled.id_for_label }}">
Allow Snoozing
</label>
<div class="form-text">Allow temporary suppression of alerts</div>
</div>
</div>
</div>
<!-- Escalation Time -->
<div class="mb-3" id="escalationTimeGroup" style="display: none;">
<label for="{{ form.escalation_minutes.id_for_label }}" class="form-label">Escalation Time (minutes)</label>
{{ form.escalation_minutes }}
{% if form.escalation_minutes.errors %}
<div class="invalid-feedback d-block">
{{ form.escalation_minutes.errors.0 }}
</div>
{% endif %}
<div class="form-text">Time before escalating unacknowledged alerts</div>
</div>
</div>
</div>
<!-- Recipients -->
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-users me-2"></i>
Alert Recipients
</h5>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary" onclick="selectAllRecipients()">
Select All
</button>
<button type="button" class="btn btn-outline-secondary" onclick="clearAllRecipients()">
Clear All
</button>
</div>
</div>
</div>
<div class="card-body">
<!-- Recipient Search -->
<div class="mb-3">
<div class="input-group">
<input type="text" class="form-control" placeholder="Search recipients..." id="recipientSearch">
<button class="btn btn-outline-secondary" type="button">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<!-- Quick Recipient Groups -->
<div class="mb-3">
<label class="form-label">Quick Select:</label>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary" onclick="selectGroup('doctors')">
Doctors
</button>
<button type="button" class="btn btn-outline-success" onclick="selectGroup('nurses')">
Nurses
</button>
<button type="button" class="btn btn-outline-info" onclick="selectGroup('admin')">
Admin Staff
</button>
<button type="button" class="btn btn-outline-warning" onclick="selectGroup('managers')">
Managers
</button>
</div>
</div>
<!-- Recipients List -->
<div class="recipients-container" style="max-height: 300px; overflow-y: auto;">
{{ form.recipients }}
</div>
{% if form.recipients.errors %}
<div class="invalid-feedback d-block">
{{ form.recipients.errors.0 }}
</div>
{% endif %}
<div class="form-text">Select users who should receive alerts from this rule</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Form Actions -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-cog me-2"></i>
Actions
</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<button type="submit" name="action" value="save" class="btn btn-primary">
<i class="fas fa-save me-1"></i>
Save Rule
</button>
<button type="submit" name="action" value="save_and_test" class="btn btn-success">
<i class="fas fa-flask me-1"></i>
Save & Test
</button>
<hr>
<button type="button" class="btn btn-outline-info" onclick="previewRule()">
<i class="fas fa-eye me-1"></i>
Preview Rule
</button>
<button type="button" class="btn btn-outline-secondary" onclick="validateRule()">
<i class="fas fa-check me-1"></i>
Validate Rule
</button>
<hr>
<a href="{% url 'communications:alert_rule_list' %}" class="btn btn-outline-danger">
<i class="fas fa-times me-1"></i>
Cancel
</a>
</div>
</div>
</div>
<!-- Rule Help -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-question-circle me-2"></i>
Help & Examples
</h5>
</div>
<div class="card-body">
<div class="accordion" id="helpAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#conditionExamples">
Condition Examples
</button>
</h2>
<div id="conditionExamples" class="accordion-collapse collapse" data-bs-parent="#helpAccordion">
<div class="accordion-body">
<small>
<strong>Patient Vitals:</strong><br>
<code>patient.temperature > 38.5</code><br>
<code>patient.heart_rate < 60</code><br><br>
<strong>Equipment:</strong><br>
<code>equipment.status == 'OFFLINE'</code><br>
<code>equipment.battery_level < 20</code><br><br>
<strong>System:</strong><br>
<code>system.cpu_usage > 90</code><br>
<code>system.disk_space < 10</code>
</small>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#severityGuide">
Severity Guidelines
</button>
</h2>
<div id="severityGuide" class="accordion-collapse collapse" data-bs-parent="#helpAccordion">
<div class="accordion-body">
<small>
<strong>Critical:</strong> Immediate life-threatening situations<br>
<strong>High:</strong> Urgent medical attention required<br>
<strong>Medium:</strong> Important but not urgent<br>
<strong>Low:</strong> Informational or routine alerts
</small>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#bestPractices">
Best Practices
</button>
</h2>
<div id="bestPractices" class="accordion-collapse collapse" data-bs-parent="#helpAccordion">
<div class="accordion-body">
<small>
• Use descriptive rule names<br>
• Set appropriate cooldown periods<br>
• Test rules before activating<br>
• Use escalation for critical alerts<br>
• Limit recipients to relevant staff<br>
• Monitor rule performance regularly
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Rule Preview -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-eye me-2"></i>
Rule Preview
</h5>
</div>
<div class="card-body">
<div id="rulePreview">
<div class="text-muted text-center py-3">
<i class="fas fa-info-circle fa-2x mb-2"></i>
<p>Fill out the form to see a preview of your alert rule</p>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- Condition Help Modal -->
<div class="modal fade" id="conditionHelpModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Condition Expression Help</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h6>Available Fields:</h6>
<ul class="list-unstyled">
<li><code>patient.temperature</code></li>
<li><code>patient.blood_pressure</code></li>
<li><code>patient.heart_rate</code></li>
<li><code>equipment.status</code></li>
<li><code>medication.stock</code></li>
<li><code>system.cpu_usage</code></li>
</ul>
</div>
<div class="col-md-6">
<h6>Operators:</h6>
<ul class="list-unstyled">
<li><code>&gt;</code> Greater than</li>
<li><code>&lt;</code> Less than</li>
<li><code>&gt;=</code> Greater or equal</li>
<li><code>&lt;=</code> Less or equal</li>
<li><code>==</code> Equals</li>
<li><code>!=</code> Not equals</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script>
// Escalation toggle
document.getElementById('{{ form.escalation_enabled.id_for_label }}').addEventListener('change', function() {
const escalationTimeGroup = document.getElementById('escalationTimeGroup');
escalationTimeGroup.style.display = this.checked ? 'block' : 'none';
});
// Expression builder
function addToExpression() {
const field = document.getElementById('fieldSelector').value;
const operator = document.getElementById('operatorSelector').value;
const value = document.getElementById('valueInput').value;
if (field && operator && value) {
const expression = `${field} ${operator} ${value}`;
const conditionField = document.getElementById('{{ form.condition_expression.id_for_label }}');
if (conditionField.value) {
conditionField.value += ' AND ' + expression;
} else {
conditionField.value = expression;
}
// Clear builder fields
document.getElementById('fieldSelector').value = '';
document.getElementById('operatorSelector').value = '';
document.getElementById('valueInput').value = '';
updatePreview();
} else {
alert('Please select field, operator, and enter a value');
}
}
// Validation functions
function validateExpression() {
const expression = document.getElementById('{{ form.condition_expression.id_for_label }}').value;
if (!expression.trim()) {
alert('Please enter a condition expression first');
return;
}
// Basic validation
const errors = [];
// Check for balanced parentheses
const openParens = (expression.match(/\(/g) || []).length;
const closeParens = (expression.match(/\)/g) || []).length;
if (openParens !== closeParens) {
errors.push('Unbalanced parentheses');
}
// Check for valid operators
const validOperators = ['>', '<', '>=', '<=', '==', '!=', 'AND', 'OR'];
const hasValidOperator = validOperators.some(op => expression.includes(op));
if (!hasValidOperator) {
errors.push('No valid operators found');
}
if (errors.length === 0) {
alert('Expression validation passed! ✓');
} else {
alert('Expression validation failed:\n\n' + errors.join('\n'));
}
}
function validateRule() {
const ruleName = document.getElementById('{{ form.rule_name.id_for_label }}').value;
const condition = document.getElementById('{{ form.condition_expression.id_for_label }}').value;
const category = document.getElementById('{{ form.category.id_for_label }}').value;
const severity = document.getElementById('{{ form.severity.id_for_label }}').value;
const errors = [];
if (!ruleName.trim()) errors.push('Rule name is required');
if (!condition.trim()) errors.push('Condition expression is required');
if (!category) errors.push('Category is required');
if (!severity) errors.push('Severity is required');
if (errors.length === 0) {
alert('Rule validation passed! ✓');
} else {
alert('Rule validation failed:\n\n' + errors.join('\n'));
}
}
// Preview functions
function updatePreview() {
const ruleName = document.getElementById('{{ form.rule_name.id_for_label }}').value;
const condition = document.getElementById('{{ form.condition_expression.id_for_label }}').value;
const category = document.getElementById('{{ form.category.id_for_label }}').value;
const severity = document.getElementById('{{ form.severity.id_for_label }}').value;
if (ruleName && condition && category && severity) {
const preview = `
<div class="alert alert-info">
<h6>${ruleName}</h6>
<p><strong>Category:</strong> ${category}</p>
<p><strong>Severity:</strong> ${severity}</p>
<p><strong>Condition:</strong> <code>${condition}</code></p>
</div>
`;
document.getElementById('rulePreview').innerHTML = preview;
}
}
function previewRule() {
updatePreview();
}
function previewTemplate() {
const templateSelect = document.getElementById('{{ form.notification_template.id_for_label }}');
const templateId = templateSelect.value;
if (templateId) {
window.open(`/communications/templates/${templateId}/preview/`, '_blank');
} else {
alert('Please select a template first');
}
}
// Recipient functions
function selectAllRecipients() {
const checkboxes = document.querySelectorAll('input[name="{{ form.recipients.name }}"]');
checkboxes.forEach(checkbox => {
checkbox.checked = true;
});
}
function clearAllRecipients() {
const checkboxes = document.querySelectorAll('input[name="{{ form.recipients.name }}"]');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
});
}
function selectGroup(group) {
// Implementation for selecting specific groups
alert(`Selecting ${group} group - functionality to be implemented`);
}
// Help functions
function showConditionHelp() {
new bootstrap.Modal(document.getElementById('conditionHelpModal')).show();
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
// Show escalation time if escalation is enabled
if (document.getElementById('{{ form.escalation_enabled.id_for_label }}').checked) {
document.getElementById('escalationTimeGroup').style.display = 'block';
}
// Add event listeners for preview updates
const previewFields = [
'{{ form.rule_name.id_for_label }}',
'{{ form.condition_expression.id_for_label }}',
'{{ form.category.id_for_label }}',
'{{ form.severity.id_for_label }}'
];
previewFields.forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field) {
field.addEventListener('input', updatePreview);
field.addEventListener('change', updatePreview);
}
});
// Recipient search functionality
document.getElementById('recipientSearch').addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const labels = document.querySelectorAll('.recipients-container label');
labels.forEach(label => {
const text = label.textContent.toLowerCase();
label.style.display = text.includes(searchTerm) ? 'block' : 'none';
});
});
updatePreview();
});
</script>
{% endblock %}