501 lines
24 KiB
HTML
501 lines
24 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}Quality Incidents{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div>
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{% url 'quality:dashboard' %}">Quality</a></li>
|
|
<li class="breadcrumb-item active">Incidents</li>
|
|
</ol>
|
|
<h1 class="page-header mb-0">Quality Incidents</h1>
|
|
</div>
|
|
<div class="ms-auto">
|
|
<div class="btn-group">
|
|
<a href="{% url 'quality:incident_form' %}" class="btn btn-primary">
|
|
<i class="fas fa-plus me-2"></i>Report Incident
|
|
</a>
|
|
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
|
<span class="visually-hidden">Toggle Dropdown</span>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="#" onclick="exportIncidents()">
|
|
<i class="fas fa-download me-2"></i>Export Incidents
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="generateReport()">
|
|
<i class="fas fa-chart-bar me-2"></i>Generate Report
|
|
</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item" href="#" onclick="bulkUpdate()">
|
|
<i class="fas fa-edit me-2"></i>Bulk Update
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="trendAnalysis()">
|
|
<i class="fas fa-chart-line me-2"></i>Trend Analysis
|
|
</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-xl-3 col-md-6">
|
|
<div class="card bg-primary text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="fs-4 fw-bold">{{ total_incidents|default:0 }}</div>
|
|
<div>Total Incidents</div>
|
|
</div>
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-exclamation-triangle fa-2x opacity-75"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6">
|
|
<div class="card bg-warning text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="fs-4 fw-bold">{{ open_incidents|default:0 }}</div>
|
|
<div>Open Incidents</div>
|
|
</div>
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-clock fa-2x opacity-75"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6">
|
|
<div class="card bg-danger text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="fs-4 fw-bold">{{ high_severity|default:0 }}</div>
|
|
<div>High Severity</div>
|
|
</div>
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-fire fa-2x opacity-75"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6">
|
|
<div class="card bg-success text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="fs-4 fw-bold">{{ resolved_this_month|default:0 }}</div>
|
|
<div>Resolved This Month</div>
|
|
</div>
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-check-circle fa-2x opacity-75"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters and Search -->
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<form method="get" class="row g-3">
|
|
<div class="col-md-3">
|
|
<label for="search" class="form-label">Search</label>
|
|
<input type="text"
|
|
class="form-control"
|
|
id="search"
|
|
name="search"
|
|
value="{{ request.GET.search }}"
|
|
placeholder="Search incidents...">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="severity" class="form-label">Severity</label>
|
|
<select class="form-select" id="severity" name="severity">
|
|
<option value="">All Severities</option>
|
|
<option value="low" {% if request.GET.severity == 'low' %}selected{% endif %}>Low</option>
|
|
<option value="medium" {% if request.GET.severity == 'medium' %}selected{% endif %}>Medium</option>
|
|
<option value="high" {% if request.GET.severity == 'high' %}selected{% endif %}>High</option>
|
|
<option value="critical" {% if request.GET.severity == 'critical' %}selected{% endif %}>Critical</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="status" class="form-label">Status</label>
|
|
<select class="form-select" id="status" name="status">
|
|
<option value="">All Status</option>
|
|
<option value="open" {% if request.GET.status == 'open' %}selected{% endif %}>Open</option>
|
|
<option value="investigating" {% if request.GET.status == 'investigating' %}selected{% endif %}>Investigating</option>
|
|
<option value="resolved" {% if request.GET.status == 'resolved' %}selected{% endif %}>Resolved</option>
|
|
<option value="closed" {% if request.GET.status == 'closed' %}selected{% endif %}>Closed</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="category" class="form-label">Category</label>
|
|
<select class="form-select" id="category" name="category">
|
|
<option value="">All Categories</option>
|
|
<option value="patient_safety" {% if request.GET.category == 'patient_safety' %}selected{% endif %}>Patient Safety</option>
|
|
<option value="medication_error" {% if request.GET.category == 'medication_error' %}selected{% endif %}>Medication Error</option>
|
|
<option value="equipment_failure" {% if request.GET.category == 'equipment_failure' %}selected{% endif %}>Equipment Failure</option>
|
|
<option value="infection_control" {% if request.GET.category == 'infection_control' %}selected{% endif %}>Infection Control</option>
|
|
<option value="documentation" {% if request.GET.category == 'documentation' %}selected{% endif %}>Documentation</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="date_range" class="form-label">Date Range</label>
|
|
<select class="form-select" id="date_range" name="date_range">
|
|
<option value="">All Dates</option>
|
|
<option value="today" {% if request.GET.date_range == 'today' %}selected{% endif %}>Today</option>
|
|
<option value="week" {% if request.GET.date_range == 'week' %}selected{% endif %}>This Week</option>
|
|
<option value="month" {% if request.GET.date_range == 'month' %}selected{% endif %}>This Month</option>
|
|
<option value="quarter" {% if request.GET.date_range == 'quarter' %}selected{% endif %}>This Quarter</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-1">
|
|
<label class="form-label"> </label>
|
|
<div class="d-grid">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Incidents Table -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="d-flex align-items-center">
|
|
<h4 class="card-title mb-0">
|
|
<i class="fas fa-list me-2"></i>
|
|
Quality Incidents
|
|
{% if object_list %}
|
|
<span class="badge bg-secondary ms-2">{{ object_list|length }}</span>
|
|
{% endif %}
|
|
</h4>
|
|
<div class="ms-auto">
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-secondary" onclick="selectAll()">
|
|
<i class="fas fa-check-square me-1"></i>Select All
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="clearSelection()">
|
|
<i class="fas fa-square me-1"></i>Clear
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary" onclick="refreshTable()">
|
|
<i class="fas fa-sync me-1"></i>Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
{% if object_list %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th width="40">
|
|
<input type="checkbox" id="selectAllCheckbox" onchange="toggleAll(this)">
|
|
</th>
|
|
<th>Incident ID</th>
|
|
<th>Title</th>
|
|
<th>Category</th>
|
|
<th>Severity</th>
|
|
<th>Status</th>
|
|
<th>Reported By</th>
|
|
<th>Date Reported</th>
|
|
<th width="120">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for incident in object_list %}
|
|
<tr>
|
|
<td>
|
|
<input type="checkbox" class="incident-checkbox" value="{{ incident.pk }}">
|
|
</td>
|
|
<td>
|
|
<div class="fw-bold">
|
|
<a href="{% url 'quality:incident_detail' incident.pk %}" class="text-decoration-none">
|
|
{{ incident.incident_id }}
|
|
</a>
|
|
</div>
|
|
{% if incident.is_confidential %}
|
|
<div class="small text-warning">
|
|
<i class="fas fa-lock me-1"></i>Confidential
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="fw-bold">{{ incident.title|truncatechars:50 }}</div>
|
|
{% if incident.patient %}
|
|
<div class="small text-muted">Patient: {{ incident.patient.get_full_name }}</div>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<i class="fas fa-{% if incident.category == 'patient_safety' %}shield-alt text-danger{% elif incident.category == 'medication_error' %}pills text-warning{% elif incident.category == 'equipment_failure' %}cog text-info{% elif incident.category == 'infection_control' %}virus text-success{% else %}file-alt text-secondary{% endif %} me-2"></i>
|
|
{{ incident.get_category_display }}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-{% if incident.severity == 'critical' %}danger{% elif incident.severity == 'high' %}warning{% elif incident.severity == 'medium' %}info{% else %}secondary{% endif %}">
|
|
{{ incident.get_severity_display }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-{% if incident.status == 'open' %}danger{% elif incident.status == 'investigating' %}warning{% elif incident.status == 'resolved' %}success{% else %}secondary{% endif %}">
|
|
{{ incident.get_status_display }}
|
|
</span>
|
|
{% if incident.days_open > 30 %}
|
|
<div class="small text-danger mt-1">
|
|
<i class="fas fa-clock me-1"></i>{{ incident.days_open }} days
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="fw-bold">{{ incident.reported_by.get_full_name }}</div>
|
|
<div class="small text-muted">{{ incident.department|default:"Unknown Dept" }}</div>
|
|
</td>
|
|
<td>
|
|
<div class="fw-bold">{{ incident.date_reported|date:"M d, Y" }}</div>
|
|
<div class="small text-muted">{{ incident.date_reported|date:"g:i A" }}</div>
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<a href="{% url 'quality:incident_detail' incident.pk %}"
|
|
class="btn btn-outline-primary"
|
|
title="View Details">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
<a href="{% url 'quality:incident_form' incident.pk %}"
|
|
class="btn btn-outline-secondary"
|
|
title="Edit">
|
|
<i class="fas fa-edit"></i>
|
|
</a>
|
|
<button type="button"
|
|
class="btn btn-outline-info"
|
|
onclick="quickUpdate({{ incident.pk }})"
|
|
title="Quick Update">
|
|
<i class="fas fa-bolt"></i>
|
|
</button>
|
|
<a href="{% url 'quality:incident_confirm_delete' incident.pk %}"
|
|
class="btn btn-outline-danger"
|
|
title="Delete">
|
|
<i class="fas fa-trash"></i>
|
|
</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if is_paginated %}
|
|
<div class="card-footer">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ paginator.count }} incidents
|
|
</div>
|
|
<nav>
|
|
<ul class="pagination pagination-sm mb-0">
|
|
{% if page_obj.has_previous %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page=1{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.severity %}&severity={{ request.GET.severity }}{% endif %}">First</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.severity %}&severity={{ request.GET.severity }}{% endif %}">Previous</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
<li class="page-item active">
|
|
<span class="page-link">{{ page_obj.number }}</span>
|
|
</li>
|
|
|
|
{% if page_obj.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.severity %}&severity={{ request.GET.severity }}{% endif %}">Next</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ paginator.num_pages }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.severity %}&severity={{ request.GET.severity }}{% endif %}">Last</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Bulk Actions -->
|
|
<div class="card-footer bg-light" id="bulkActions" style="display: none;">
|
|
<div class="d-flex align-items-center">
|
|
<div class="me-3">
|
|
<span id="selectedCount">0</span> incident(s) selected
|
|
</div>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-warning" onclick="bulkUpdateStatus('investigating')">
|
|
<i class="fas fa-search me-1"></i>Start Investigation
|
|
</button>
|
|
<button type="button" class="btn btn-success" onclick="bulkUpdateStatus('resolved')">
|
|
<i class="fas fa-check me-1"></i>Mark Resolved
|
|
</button>
|
|
<button type="button" class="btn btn-info" onclick="bulkAssign()">
|
|
<i class="fas fa-user me-1"></i>Assign
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary" onclick="bulkExport()">
|
|
<i class="fas fa-download me-1"></i>Export
|
|
</button>
|
|
<button type="button" class="btn btn-danger" onclick="bulkDelete()">
|
|
<i class="fas fa-trash me-1"></i>Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-exclamation-triangle fa-3x text-muted mb-3"></i>
|
|
<h5>No Incidents Found</h5>
|
|
<p class="text-muted">No quality incidents match your current filters.</p>
|
|
<a href="{% url 'quality:incident_form' %}" class="btn btn-primary">
|
|
<i class="fas fa-plus me-2"></i>Report First Incident
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
updateBulkActions();
|
|
|
|
// Auto-refresh every 60 seconds for real-time updates
|
|
setInterval(function() {
|
|
if (document.visibilityState === 'visible') {
|
|
refreshTable();
|
|
}
|
|
}, 60000);
|
|
});
|
|
|
|
function toggleAll(checkbox) {
|
|
const checkboxes = document.querySelectorAll('.incident-checkbox');
|
|
checkboxes.forEach(cb => cb.checked = checkbox.checked);
|
|
updateBulkActions();
|
|
}
|
|
|
|
function selectAll() {
|
|
const checkboxes = document.querySelectorAll('.incident-checkbox');
|
|
checkboxes.forEach(cb => cb.checked = true);
|
|
document.getElementById('selectAllCheckbox').checked = true;
|
|
updateBulkActions();
|
|
}
|
|
|
|
function clearSelection() {
|
|
const checkboxes = document.querySelectorAll('.incident-checkbox');
|
|
checkboxes.forEach(cb => cb.checked = false);
|
|
document.getElementById('selectAllCheckbox').checked = false;
|
|
updateBulkActions();
|
|
}
|
|
|
|
function updateBulkActions() {
|
|
const checkboxes = document.querySelectorAll('.incident-checkbox:checked');
|
|
const count = checkboxes.length;
|
|
const bulkActions = document.getElementById('bulkActions');
|
|
const selectedCount = document.getElementById('selectedCount');
|
|
|
|
if (count > 0) {
|
|
bulkActions.style.display = 'block';
|
|
selectedCount.textContent = count;
|
|
} else {
|
|
bulkActions.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Add event listeners to checkboxes
|
|
document.addEventListener('change', function(e) {
|
|
if (e.target.classList.contains('incident-checkbox')) {
|
|
updateBulkActions();
|
|
}
|
|
});
|
|
|
|
function getSelectedIncidents() {
|
|
const checkboxes = document.querySelectorAll('.incident-checkbox:checked');
|
|
return Array.from(checkboxes).map(cb => cb.value);
|
|
}
|
|
|
|
function quickUpdate(incidentId) {
|
|
// In a real implementation, this would open a quick update modal
|
|
const status = prompt('Update status to (open/investigating/resolved/closed):');
|
|
if (status && ['open', 'investigating', 'resolved', 'closed'].includes(status)) {
|
|
console.log('Updating incident', incidentId, 'to status', status);
|
|
location.reload();
|
|
}
|
|
}
|
|
|
|
function bulkUpdateStatus(status) {
|
|
const selected = getSelectedIncidents();
|
|
if (selected.length === 0) return;
|
|
|
|
const statusText = status.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
if (confirm(`Update status to "${statusText}" for ${selected.length} selected incident(s)?`)) {
|
|
console.log('Updating status to', status, 'for incidents:', selected);
|
|
location.reload();
|
|
}
|
|
}
|
|
|
|
function bulkAssign() {
|
|
const selected = getSelectedIncidents();
|
|
if (selected.length === 0) return;
|
|
|
|
// In a real implementation, this would open an assignment modal
|
|
alert('Bulk assignment functionality would be implemented here.');
|
|
}
|
|
|
|
function bulkExport() {
|
|
const selected = getSelectedIncidents();
|
|
if (selected.length === 0) return;
|
|
|
|
console.log('Exporting incidents:', selected);
|
|
alert('Export functionality would be implemented here.');
|
|
}
|
|
|
|
function bulkDelete() {
|
|
const selected = getSelectedIncidents();
|
|
if (selected.length === 0) return;
|
|
|
|
if (confirm(`Are you sure you want to delete ${selected.length} selected incident(s)? This action cannot be undone.`)) {
|
|
console.log('Deleting incidents:', selected);
|
|
location.reload();
|
|
}
|
|
}
|
|
|
|
function refreshTable() {
|
|
location.reload();
|
|
}
|
|
|
|
function exportIncidents() {
|
|
alert('Export all incidents functionality would be implemented here.');
|
|
}
|
|
|
|
function generateReport() {
|
|
alert('Generate report functionality would be implemented here.');
|
|
}
|
|
|
|
function bulkUpdate() {
|
|
alert('Bulk update functionality would be implemented here.');
|
|
}
|
|
|
|
function trendAnalysis() {
|
|
alert('Trend analysis functionality would be implemented here.');
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|