606 lines
22 KiB
HTML
606 lines
22 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Reference Range - {{ reference_range.test_type.name }}{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.range-info-card {
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.range-visual {
|
|
background: #fff;
|
|
border: 2px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 2rem;
|
|
margin: 1rem 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.range-bar {
|
|
height: 40px;
|
|
border-radius: 20px;
|
|
position: relative;
|
|
margin: 2rem 0;
|
|
background: linear-gradient(to right,
|
|
#dc3545 0%, #dc3545 10%,
|
|
#ffc107 10%, #ffc107 20%,
|
|
#28a745 20%, #28a745 80%,
|
|
#ffc107 80%, #ffc107 90%,
|
|
#dc3545 90%, #dc3545 100%);
|
|
}
|
|
|
|
.range-marker {
|
|
position: absolute;
|
|
top: -10px;
|
|
width: 2px;
|
|
height: 60px;
|
|
background: #000;
|
|
}
|
|
|
|
.range-marker::after {
|
|
content: attr(data-value);
|
|
position: absolute;
|
|
top: 65px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
font-size: 0.75rem;
|
|
font-weight: bold;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.range-legend {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-top: 1rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.legend-color {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.stat-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
.stat-item {
|
|
background: #fff;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
color: #495057;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.history-timeline {
|
|
position: relative;
|
|
padding-left: 2rem;
|
|
}
|
|
|
|
.history-timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0.75rem;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background: #dee2e6;
|
|
}
|
|
|
|
.timeline-item {
|
|
position: relative;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.timeline-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -2.25rem;
|
|
top: 0.5rem;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: #007bff;
|
|
border: 2px solid #fff;
|
|
box-shadow: 0 0 0 2px #dee2e6;
|
|
}
|
|
|
|
.timeline-content {
|
|
background: #fff;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.badge-custom {
|
|
padding: 0.375rem 0.75rem;
|
|
border-radius: 0.375rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.badge-adult { background: #d4edda; color: #155724; }
|
|
.badge-pediatric { background: #d1ecf1; color: #0c5460; }
|
|
.badge-geriatric { background: #fff3cd; color: #856404; }
|
|
.badge-male { background: #cce5ff; color: #004085; }
|
|
.badge-female { background: #f8d7da; color: #721c24; }
|
|
.badge-both { background: #e2e3e5; color: #383d41; }
|
|
|
|
@media (max-width: 768px) {
|
|
.range-visual {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.stat-grid {
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
}
|
|
|
|
.range-bar {
|
|
height: 30px;
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
.range-marker {
|
|
height: 50px;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="content" class="app-content">
|
|
<!-- Page Header -->
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div>
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'laboratory:dashboard' %}">Laboratory</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'laboratory:reference_range_list' %}">Reference Ranges</a></li>
|
|
<li class="breadcrumb-item active">{{ reference_range.test_type.name }}</li>
|
|
</ol>
|
|
<h1 class="page-header mb-0">
|
|
<i class="fas fa-ruler me-2"></i>{{ reference_range.test_type.name }}
|
|
</h1>
|
|
</div>
|
|
<div class="ms-auto">
|
|
<div class="btn-group">
|
|
<a href="{% url 'laboratory:reference_range_list' %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-arrow-left me-1"></i>Back to List
|
|
</a>
|
|
<a href="{% url 'laboratory:reference_range_update' reference_range.pk %}" class="btn btn-outline-primary">
|
|
<i class="fas fa-edit me-1"></i>Edit
|
|
</a>
|
|
<button type="button" class="btn btn-outline-info" onclick="duplicateRange()">
|
|
<i class="fas fa-copy me-1"></i>Duplicate
|
|
</button>
|
|
<button type="button" class="btn btn-outline-success" onclick="printRange()">
|
|
<i class="fas fa-print me-1"></i>Print
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-8">
|
|
<!-- Range Information -->
|
|
<div class="range-info-card">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h5 class="mb-3">
|
|
<i class="fas fa-flask me-2"></i>Test Information
|
|
</h5>
|
|
<div class="mb-2">
|
|
<strong>Test Name:</strong> {{ reference_range.test_type.name }}
|
|
</div>
|
|
<div class="mb-2">
|
|
<strong>Test Code:</strong> {{ reference_range.test_type.code }}
|
|
</div>
|
|
<div class="mb-2">
|
|
<strong>Category:</strong> {{ reference_range.test_type.category.name }}
|
|
</div>
|
|
<div class="mb-2">
|
|
<strong>Method:</strong> {{ reference_range.test_type.method|default:"Not specified" }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h5 class="mb-3">
|
|
<i class="fas fa-users me-2"></i>Demographics
|
|
</h5>
|
|
<div class="mb-2">
|
|
<strong>Age Group:</strong>
|
|
<span class="badge-custom badge-{{ reference_range.age_group }}">
|
|
{{ reference_range.get_age_group_display }}
|
|
</span>
|
|
</div>
|
|
{% if reference_range.age_min or reference_range.age_max %}
|
|
<div class="mb-2">
|
|
<strong>Age Range:</strong>
|
|
{% if reference_range.age_min %}{{ reference_range.age_min }}{% endif %}
|
|
{% if reference_range.age_min and reference_range.age_max %} - {% endif %}
|
|
{% if reference_range.age_max %}{{ reference_range.age_max }}{% endif %}
|
|
{{ reference_range.age_unit }}
|
|
</div>
|
|
{% endif %}
|
|
<div class="mb-2">
|
|
<strong>Gender:</strong>
|
|
<span class="badge-custom badge-{{ reference_range.gender }}">
|
|
{{ reference_range.get_gender_display }}
|
|
</span>
|
|
</div>
|
|
<div class="mb-2">
|
|
<strong>Status:</strong>
|
|
{% if reference_range.is_active %}
|
|
<span class="badge bg-success">Active</span>
|
|
{% else %}
|
|
<span class="badge bg-danger">Inactive</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Range Visualization -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-chart-bar me-2"></i>Reference Range Visualization
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="range-visual">
|
|
<h6 class="mb-3">{{ reference_range.test_type.name }} Reference Range</h6>
|
|
|
|
<div class="stat-grid">
|
|
{% if reference_range.critical_low %}
|
|
<div class="stat-item">
|
|
<div class="stat-value text-danger">{{ reference_range.critical_low }}</div>
|
|
<div class="stat-label">Critical Low</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if reference_range.min_value %}
|
|
<div class="stat-item">
|
|
<div class="stat-value text-warning">{{ reference_range.min_value }}</div>
|
|
<div class="stat-label">Normal Low</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if reference_range.max_value %}
|
|
<div class="stat-item">
|
|
<div class="stat-value text-warning">{{ reference_range.max_value }}</div>
|
|
<div class="stat-label">Normal High</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if reference_range.critical_high %}
|
|
<div class="stat-item">
|
|
<div class="stat-value text-danger">{{ reference_range.critical_high }}</div>
|
|
<div class="stat-label">Critical High</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if reference_range.min_value and reference_range.max_value %}
|
|
<div class="range-bar" id="rangeBar">
|
|
<!-- Range markers will be added by JavaScript -->
|
|
</div>
|
|
|
|
<div class="range-legend">
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #dc3545;"></div>
|
|
<span>Critical</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #ffc107;"></div>
|
|
<span>Abnormal</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #28a745;"></div>
|
|
<span>Normal</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="mt-3">
|
|
<strong>Unit:</strong> {{ reference_range.unit|default:"Not specified" }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Additional Information -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-info-circle me-2"></i>Additional Information
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6>Clinical Information</h6>
|
|
{% if reference_range.clinical_significance %}
|
|
<div class="mb-3">
|
|
<strong>Clinical Significance:</strong>
|
|
<p class="mt-1">{{ reference_range.clinical_significance }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if reference_range.interpretation_notes %}
|
|
<div class="mb-3">
|
|
<strong>Interpretation Notes:</strong>
|
|
<p class="mt-1">{{ reference_range.interpretation_notes }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6>Technical Information</h6>
|
|
{% if reference_range.methodology %}
|
|
<div class="mb-3">
|
|
<strong>Methodology:</strong>
|
|
<p class="mt-1">{{ reference_range.methodology }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if reference_range.reference_source %}
|
|
<div class="mb-3">
|
|
<strong>Reference Source:</strong>
|
|
<p class="mt-1">{{ reference_range.reference_source }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if reference_range.conditions %}
|
|
<div class="mt-3">
|
|
<h6>Special Conditions</h6>
|
|
<p>{{ reference_range.conditions }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="col-lg-4">
|
|
<!-- Quick Stats -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-chart-pie me-2"></i>Usage Statistics
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="stat-item mb-3">
|
|
<div class="stat-value">{{ usage_stats.total_tests }}</div>
|
|
<div class="stat-label">Total Tests (Last 30 days)</div>
|
|
</div>
|
|
<div class="stat-item mb-3">
|
|
<div class="stat-value">{{ usage_stats.normal_results }}%</div>
|
|
<div class="stat-label">Normal Results</div>
|
|
</div>
|
|
<div class="stat-item mb-3">
|
|
<div class="stat-value">{{ usage_stats.abnormal_results }}%</div>
|
|
<div class="stat-label">Abnormal Results</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">{{ usage_stats.critical_results }}%</div>
|
|
<div class="stat-label">Critical Results</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Related Ranges -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-link me-2"></i>Related Ranges
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% for related_range in related_ranges %}
|
|
<div class="d-flex align-items-center mb-2">
|
|
<div class="flex-grow-1">
|
|
<div class="fw-bold">{{ related_range.test_type.name }}</div>
|
|
<small class="text-muted">
|
|
{{ related_range.get_age_group_display }} - {{ related_range.get_gender_display }}
|
|
</small>
|
|
</div>
|
|
<div class="text-end">
|
|
<a href="{% url 'laboratory:reference_range_detail' related_range.pk %}"
|
|
class="btn btn-outline-primary btn-sm">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-muted text-center py-3">
|
|
<i class="fas fa-link fa-2x mb-2"></i>
|
|
<p>No related ranges</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Change History -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-history me-2"></i>Change History
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="history-timeline">
|
|
{% for change in change_history %}
|
|
<div class="timeline-item">
|
|
<div class="timeline-content">
|
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
<strong>{{ change.action }}</strong>
|
|
<small class="text-muted">{{ change.created_at|date:"M d, Y H:i" }}</small>
|
|
</div>
|
|
<div class="mb-2">
|
|
<strong>By:</strong> {{ change.user.get_full_name|default:change.user.username }}
|
|
</div>
|
|
{% if change.changes %}
|
|
<div class="small">
|
|
<strong>Changes:</strong>
|
|
<ul class="mb-0">
|
|
{% for field, values in change.changes.items %}
|
|
<li>{{ field }}: {{ values.old }} → {{ values.new }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
{% if change.notes %}
|
|
<div class="small mt-2">
|
|
<strong>Notes:</strong> {{ change.notes }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-muted text-center py-3">
|
|
<i class="fas fa-history fa-2x mb-2"></i>
|
|
<p>No change history</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Initialize range visualization
|
|
initializeRangeVisualization();
|
|
});
|
|
|
|
function initializeRangeVisualization() {
|
|
const rangeBar = document.getElementById('rangeBar');
|
|
if (!rangeBar) return;
|
|
|
|
const criticalLow = {{ reference_range.critical_low|default:"null" }};
|
|
const minValue = {{ reference_range.min_value|default:"null" }};
|
|
const maxValue = {{ reference_range.max_value|default:"null" }};
|
|
const criticalHigh = {{ reference_range.critical_high|default:"null" }};
|
|
|
|
// Calculate the full range for positioning
|
|
const values = [criticalLow, minValue, maxValue, criticalHigh].filter(v => v !== null);
|
|
if (values.length < 2) return;
|
|
|
|
const minRange = Math.min(...values);
|
|
const maxRange = Math.max(...values);
|
|
const rangeSpan = maxRange - minRange;
|
|
|
|
// Add markers for each value
|
|
if (criticalLow !== null) {
|
|
addRangeMarker(rangeBar, criticalLow, minRange, rangeSpan, 'Critical Low');
|
|
}
|
|
|
|
if (minValue !== null) {
|
|
addRangeMarker(rangeBar, minValue, minRange, rangeSpan, 'Normal Low');
|
|
}
|
|
|
|
if (maxValue !== null) {
|
|
addRangeMarker(rangeBar, maxValue, minRange, rangeSpan, 'Normal High');
|
|
}
|
|
|
|
if (criticalHigh !== null) {
|
|
addRangeMarker(rangeBar, criticalHigh, minRange, rangeSpan, 'Critical High');
|
|
}
|
|
}
|
|
|
|
function addRangeMarker(container, value, minRange, rangeSpan, label) {
|
|
const position = ((value - minRange) / rangeSpan) * 100;
|
|
|
|
const marker = document.createElement('div');
|
|
marker.className = 'range-marker';
|
|
marker.style.left = position + '%';
|
|
marker.setAttribute('data-value', value + ' ' + '{{ reference_range.unit|default:"" }}');
|
|
marker.title = label + ': ' + value;
|
|
|
|
container.appendChild(marker);
|
|
}
|
|
|
|
function duplicateRange() {
|
|
if (confirm('Create a duplicate of this reference range?')) {
|
|
$.ajax({
|
|
url: '{% url "laboratory:duplicate_reference_range" %}',
|
|
method: 'POST',
|
|
data: {
|
|
'range_id': '{{ reference_range.pk }}',
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
window.location.href = '{% url "laboratory:reference_range_detail" %}' + response.new_range_id + '/';
|
|
} else {
|
|
alert('Error duplicating range: ' + response.error);
|
|
}
|
|
},
|
|
error: function() {
|
|
alert('Error duplicating range');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function printRange() {
|
|
window.print();
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
@media print {
|
|
.btn-group, .breadcrumb {
|
|
display: none !important;
|
|
}
|
|
|
|
.page-header {
|
|
margin-bottom: 1rem !important;
|
|
}
|
|
|
|
.card {
|
|
border: 1px solid #000 !important;
|
|
break-inside: avoid;
|
|
}
|
|
|
|
.range-visual {
|
|
border: 2px solid #000 !important;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|