588 lines
29 KiB
HTML
588 lines
29 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}{{ object.rule_name }} - Alert Rule Details{% 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">Alert Rule Details</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">Details</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Main Content -->
|
|
<div class="col-lg-8">
|
|
<!-- Alert Rule Overview -->
|
|
<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-exclamation-triangle me-2"></i>
|
|
{{ object.rule_name }}
|
|
</h5>
|
|
<div class="d-flex gap-2">
|
|
<span class="badge bg-{% if object.category == 'SYSTEM' %}primary{% elif object.category == 'PATIENT' %}success{% elif object.category == 'EQUIPMENT' %}warning{% elif object.category == 'MEDICATION' %}info{% else %}secondary{% endif %} fs-6">
|
|
{{ object.get_category_display }}
|
|
</span>
|
|
<span class="badge bg-{% if object.severity == 'CRITICAL' %}danger{% elif object.severity == 'HIGH' %}warning{% elif object.severity == 'MEDIUM' %}info{% else %}secondary{% endif %} fs-6">
|
|
{{ object.get_severity_display }}
|
|
</span>
|
|
<span class="badge bg-{% if object.is_active %}success{% else %}secondary{% endif %} fs-6">
|
|
{% if object.is_active %}Active{% else %}Inactive{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- Rule Information -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<div class="table-responsive">
|
|
<table class="table table-borderless">
|
|
<tr>
|
|
<td class="fw-bold text-muted">Rule Name:</td>
|
|
<td>{{ object.rule_name }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Category:</td>
|
|
<td>{{ object.get_category_display }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Severity:</td>
|
|
<td>{{ object.get_severity_display }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Event Type:</td>
|
|
<td>{{ object.event_type|default:"Any" }}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="table-responsive">
|
|
<table class="table table-borderless">
|
|
<tr>
|
|
<td class="fw-bold text-muted">Created:</td>
|
|
<td>{{ object.created_at|date:"M d, Y g:i A" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Last Updated:</td>
|
|
<td>{{ object.updated_at|date:"M d, Y g:i A" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Last Triggered:</td>
|
|
<td>
|
|
{% if object.last_triggered %}
|
|
{{ object.last_triggered|date:"M d, Y g:i A" }}
|
|
{% else %}
|
|
<span class="text-muted">Never</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Trigger Count:</td>
|
|
<td>{{ object.trigger_count|default:0 }} times</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
{% if object.description %}
|
|
<div class="mb-4">
|
|
<h6 class="text-muted mb-2">Description:</h6>
|
|
<div class="bg-light p-3 rounded">
|
|
{{ object.description|linebreaks }}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Condition Expression -->
|
|
<div class="mb-4">
|
|
<h6 class="text-muted mb-2">Condition Expression:</h6>
|
|
<div class="bg-dark p-3 rounded">
|
|
<code class="text-light">{{ object.condition_expression }}</code>
|
|
</div>
|
|
<small class="text-muted">This condition is evaluated to determine when the alert should trigger</small>
|
|
</div>
|
|
|
|
<!-- Condition Parameters -->
|
|
{% if object.condition_parameters %}
|
|
<div class="mb-4">
|
|
<h6 class="text-muted mb-2">Condition Parameters:</h6>
|
|
<div class="bg-light p-3 rounded">
|
|
<pre class="mb-0">{{ object.condition_parameters|pprint }}</pre>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Notification Settings -->
|
|
<div class="mb-4">
|
|
<h6 class="text-muted mb-2">Notification Settings:</h6>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="table-responsive">
|
|
<table class="table table-borderless table-sm">
|
|
<tr>
|
|
<td class="fw-bold text-muted">Cooldown Period:</td>
|
|
<td>{{ object.cooldown_minutes|default:0 }} minutes</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Max Triggers/Day:</td>
|
|
<td>{{ object.max_triggers_per_day|default:"Unlimited" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Auto Resolve:</td>
|
|
<td>
|
|
{% if object.auto_resolve %}
|
|
<span class="text-success"><i class="fas fa-check me-1"></i>Enabled</span>
|
|
{% else %}
|
|
<span class="text-muted"><i class="fas fa-times me-1"></i>Disabled</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="table-responsive">
|
|
<table class="table table-borderless table-sm">
|
|
<tr>
|
|
<td class="fw-bold text-muted">Escalation:</td>
|
|
<td>
|
|
{% if object.escalation_enabled %}
|
|
<span class="text-warning"><i class="fas fa-arrow-up me-1"></i>Enabled</span>
|
|
{% else %}
|
|
<span class="text-muted"><i class="fas fa-times me-1"></i>Disabled</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Escalation Time:</td>
|
|
<td>{{ object.escalation_minutes|default:"N/A" }} minutes</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Snooze Enabled:</td>
|
|
<td>
|
|
{% if object.snooze_enabled %}
|
|
<span class="text-info"><i class="fas fa-clock me-1"></i>Enabled</span>
|
|
{% else %}
|
|
<span class="text-muted"><i class="fas fa-times me-1"></i>Disabled</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recipients -->
|
|
{% if object.recipients.exists %}
|
|
<div class="mb-4">
|
|
<h6 class="text-muted mb-2">Alert Recipients:</h6>
|
|
<div class="row">
|
|
{% for recipient in object.recipients.all %}
|
|
<div class="col-md-6 mb-2">
|
|
<div class="d-flex align-items-center bg-light p-2 rounded">
|
|
<div class="avatar-xs me-2">
|
|
<div class="avatar-title bg-soft-primary text-primary rounded-circle">
|
|
{{ recipient.first_name.0 }}{{ recipient.last_name.0 }}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h6 class="mb-0">{{ recipient.get_full_name }}</h6>
|
|
<small class="text-muted">{{ recipient.email }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Metadata -->
|
|
{% if object.metadata %}
|
|
<div class="mb-4">
|
|
<h6 class="text-muted mb-2">Additional Metadata:</h6>
|
|
<div class="bg-light p-3 rounded">
|
|
<pre class="mb-0">{{ object.metadata|pprint }}</pre>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Alert Instances -->
|
|
{% if recent_instances %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-history me-2"></i>
|
|
Recent Alert Instances
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-nowrap table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Triggered</th>
|
|
<th>Status</th>
|
|
<th>Message</th>
|
|
<th>Acknowledged</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for instance in recent_instances %}
|
|
<tr>
|
|
<td>{{ instance.triggered_at|date:"M d, Y g:i A" }}</td>
|
|
<td>
|
|
<span class="badge bg-{% if instance.status == 'RESOLVED' %}success{% elif instance.status == 'ACKNOWLEDGED' %}info{% elif instance.status == 'ESCALATED' %}warning{% else %}danger{% endif %}">
|
|
{{ instance.get_status_display }}
|
|
</span>
|
|
</td>
|
|
<td>{{ instance.message|truncatechars:50 }}</td>
|
|
<td>
|
|
{% if instance.acknowledged_at %}
|
|
<span class="text-success">
|
|
<i class="fas fa-check me-1"></i>
|
|
{{ instance.acknowledged_at|date:"M d, g:i A" }}
|
|
</span>
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<a href="{% url 'communications:alert_instance_detail' instance.pk %}" class="btn btn-sm btn-outline-primary">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="mt-3">
|
|
<a href="{% url 'communications:alert_instance_list' %}?rule={{ object.id }}" class="btn btn-outline-secondary">
|
|
View All Instances
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Rule Performance -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-chart-line me-2"></i>
|
|
Performance Metrics
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<div class="text-center p-3 bg-light rounded">
|
|
<i class="fas fa-bell fa-2x text-primary mb-2"></i>
|
|
<h6>{{ object.trigger_count|default:0 }}</h6>
|
|
<small class="text-muted">Total Triggers</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="text-center p-3 bg-light rounded">
|
|
<i class="fas fa-calendar-week fa-2x text-success mb-2"></i>
|
|
<h6>{{ weekly_triggers|default:0 }}</h6>
|
|
<small class="text-muted">This Week</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="text-center p-3 bg-light rounded">
|
|
<i class="fas fa-percentage fa-2x text-info mb-2"></i>
|
|
<h6>{{ accuracy_rate|default:0 }}%</h6>
|
|
<small class="text-muted">Accuracy Rate</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="text-center p-3 bg-light rounded">
|
|
<i class="fas fa-clock fa-2x text-warning mb-2"></i>
|
|
<h6>{{ avg_response_time|default:0 }}s</h6>
|
|
<small class="text-muted">Avg Response</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="col-lg-4">
|
|
<!-- Quick Actions -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-bolt me-2"></i>
|
|
Quick Actions
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-grid gap-2">
|
|
<button class="btn btn-success" onclick="testRule()">
|
|
<i class="fas fa-flask me-1"></i>
|
|
Test Rule
|
|
</button>
|
|
<a href="{% url 'communications:alert_rule_update' object.pk %}" class="btn btn-primary">
|
|
<i class="fas fa-edit me-1"></i>
|
|
Edit Rule
|
|
</a>
|
|
|
|
<hr>
|
|
|
|
{% if object.is_active %}
|
|
<button class="btn btn-outline-warning" onclick="toggleRule(false)">
|
|
<i class="fas fa-pause me-1"></i>
|
|
Disable Rule
|
|
</button>
|
|
{% else %}
|
|
<button class="btn btn-outline-success" onclick="toggleRule(true)">
|
|
<i class="fas fa-play me-1"></i>
|
|
Enable Rule
|
|
</button>
|
|
{% endif %}
|
|
|
|
<button class="btn btn-outline-secondary" onclick="duplicateRule()">
|
|
<i class="fas fa-copy me-1"></i>
|
|
Duplicate Rule
|
|
</button>
|
|
<button class="btn btn-outline-info" onclick="exportRule()">
|
|
<i class="fas fa-download me-1"></i>
|
|
Export Rule
|
|
</button>
|
|
|
|
<hr>
|
|
|
|
{% if object.snooze_enabled %}
|
|
<button class="btn btn-outline-secondary" onclick="snoozeRule()">
|
|
<i class="fas fa-clock me-1"></i>
|
|
Snooze Rule
|
|
</button>
|
|
{% endif %}
|
|
|
|
<a href="{% url 'communications:alert_rule_delete' object.pk %}" class="btn btn-outline-danger">
|
|
<i class="fas fa-trash me-1"></i>
|
|
Delete Rule
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Rule Status -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
Rule Status
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-borderless table-sm">
|
|
<tr>
|
|
<td class="fw-bold text-muted">Current Status:</td>
|
|
<td>
|
|
{% if object.is_active %}
|
|
<span class="text-success"><i class="fas fa-check-circle me-1"></i>Active</span>
|
|
{% else %}
|
|
<span class="text-secondary"><i class="fas fa-pause-circle me-1"></i>Inactive</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Health Status:</td>
|
|
<td>
|
|
<span class="text-success"><i class="fas fa-heart me-1"></i>Healthy</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Last Check:</td>
|
|
<td>{{ last_evaluation|default:"Never"|date:"M d, g:i A" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Next Check:</td>
|
|
<td>{{ next_evaluation|default:"N/A"|date:"M d, g:i A" }}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Rule Configuration -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-cog me-2"></i>
|
|
Configuration Summary
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-borderless table-sm">
|
|
<tr>
|
|
<td class="fw-bold text-muted">Evaluation Frequency:</td>
|
|
<td>{{ object.evaluation_frequency|default:"Real-time" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Notification Channels:</td>
|
|
<td>{{ object.notification_channels.count|default:0 }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Recipients:</td>
|
|
<td>{{ object.recipients.count|default:0 }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold text-muted">Template:</td>
|
|
<td>
|
|
{% if object.notification_template %}
|
|
<a href="{% url 'communications:notification_template_detail' object.notification_template.pk %}">
|
|
{{ object.notification_template.template_name }}
|
|
</a>
|
|
{% else %}
|
|
<span class="text-muted">Default</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Related Rules -->
|
|
{% if related_rules %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-link me-2"></i>
|
|
Related Rules
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% for rule in related_rules %}
|
|
<div class="d-flex align-items-center mb-2">
|
|
<div class="avatar-xs me-2">
|
|
<div class="avatar-title bg-soft-primary text-primary rounded">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<h6 class="mb-0">
|
|
<a href="{% url 'communications:alert_rule_detail' rule.pk %}">
|
|
{{ rule.rule_name|truncatechars:25 }}
|
|
</a>
|
|
</h6>
|
|
<small class="text-muted">{{ rule.get_category_display }}</small>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
function testRule() {
|
|
if (confirm('Test this alert rule with current system data?')) {
|
|
fetch(`{% url 'communications:alert_rule_detail' object.pk %}test/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json',
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert(`Test completed!\n\nResult: ${data.result}\nCondition met: ${data.condition_met ? 'Yes' : 'No'}\nExecution time: ${data.execution_time}ms`);
|
|
} else {
|
|
alert('Error testing rule: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function toggleRule(activate) {
|
|
const action = activate ? 'enable' : 'disable';
|
|
if (confirm(`${action.charAt(0).toUpperCase() + action.slice(1)} this alert rule?`)) {
|
|
fetch(`{% url 'communications:alert_rule_detail' object.pk %}toggle/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ active: activate })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
alert(`Error ${action}ing rule: ` + data.error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function duplicateRule() {
|
|
if (confirm('Create a copy of this alert rule?')) {
|
|
window.location.href = '{% url "communications:alert_rule_create" %}?duplicate={{ object.id }}';
|
|
}
|
|
}
|
|
|
|
function exportRule() {
|
|
window.open(`{% url 'communications:alert_rule_detail' object.pk %}export/`, '_blank');
|
|
}
|
|
|
|
function snoozeRule() {
|
|
const minutes = prompt('Snooze this rule for how many minutes?', '60');
|
|
if (minutes && !isNaN(minutes)) {
|
|
fetch(`{% url 'communications:alert_rule_detail' object.pk %}snooze/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ minutes: parseInt(minutes) })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert(`Rule snoozed for ${minutes} minutes`);
|
|
location.reload();
|
|
} else {
|
|
alert('Error snoozing rule: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|