hospital-management/templates/emr/partials/clinical_timeline.html
2025-08-12 13:33:25 +03:00

643 lines
24 KiB
HTML

{% load static %}
<!-- Clinical Timeline Widget -->
<div class="clinical-timeline-widget">
{% if timeline_events %}
<div class="timeline-container">
<!-- Timeline Header -->
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">
<i class="fa fa-history me-2"></i>Clinical Timeline
</h6>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-secondary" onclick="filterTimeline('all')" id="filter-all">All</button>
<button class="btn btn-outline-secondary" onclick="filterTimeline('encounters')" id="filter-encounters">Encounters</button>
<button class="btn btn-outline-secondary" onclick="filterTimeline('vitals')" id="filter-vitals">Vitals</button>
<button class="btn btn-outline-secondary" onclick="filterTimeline('notes')" id="filter-notes">Notes</button>
<button class="btn btn-outline-secondary" onclick="filterTimeline('problems')" id="filter-problems">Problems</button>
</div>
</div>
<!-- Timeline Period Selector -->
<div class="row mb-3">
<div class="col-md-6">
<select class="form-select form-select-sm" id="timeline-period" onchange="loadTimelinePeriod()">
<option value="24h">Last 24 Hours</option>
<option value="7d" selected>Last 7 Days</option>
<option value="30d">Last 30 Days</option>
<option value="90d">Last 90 Days</option>
<option value="1y">Last Year</option>
<option value="all">All Time</option>
</select>
</div>
<div class="col-md-6">
<div class="input-group input-group-sm">
<input type="text" class="form-control" id="timeline-search" placeholder="Search timeline...">
<button class="btn btn-outline-secondary" type="button" onclick="searchTimeline()">
<i class="fa fa-search"></i>
</button>
</div>
</div>
</div>
<!-- Timeline Events -->
<div class="timeline" id="clinical-timeline">
{% for event in timeline_events %}
<div class="timeline-item" data-type="{{ event.type }}" data-date="{{ event.date|date:'Y-m-d' }}">
<div class="timeline-time">
<div class="timeline-date">{{ event.date|date:"M d" }}</div>
<div class="timeline-hour">{{ event.date|time:"H:i" }}</div>
</div>
<div class="timeline-icon bg-{{ event.type_color }}">
<i class="fa fa-{{ event.icon }}"></i>
</div>
<div class="timeline-body">
<div class="timeline-header">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="timeline-title mb-1">{{ event.title }}</h6>
<div class="timeline-meta">
<span class="badge bg-{{ event.type_color }} me-2">{{ event.type_display }}</span>
{% if event.provider %}
<span class="text-muted small">by {{ event.provider.get_full_name }}</span>
{% endif %}
{% if event.location %}
<span class="text-muted small">• {{ event.location }}</span>
{% endif %}
</div>
</div>
<div class="timeline-actions">
{% if event.can_view %}
<a href="{{ event.detail_url }}" class="btn btn-xs btn-outline-primary" title="View Details">
<i class="fa fa-eye"></i>
</a>
{% endif %}
{% if event.can_edit %}
<a href="{{ event.edit_url }}" class="btn btn-xs btn-outline-secondary" title="Edit">
<i class="fa fa-edit"></i>
</a>
{% endif %}
</div>
</div>
</div>
<div class="timeline-content">
{% if event.type == 'encounter' %}
<div class="row">
<div class="col-md-6">
<div class="small">
<strong>Type:</strong> {{ event.encounter_type_display }}<br>
<strong>Status:</strong>
<span class="badge bg-{{ event.status_color }}">{{ event.status_display }}</span>
</div>
</div>
<div class="col-md-6">
<div class="small">
{% if event.chief_complaint %}
<strong>Chief Complaint:</strong> {{ event.chief_complaint|truncatechars:50 }}
{% endif %}
</div>
</div>
</div>
{% elif event.type == 'vital_signs' %}
<div class="row">
<div class="col-md-12">
<div class="vital-signs-summary">
{% if event.temperature %}
<span class="vital-item">
<i class="fa fa-thermometer-half text-danger"></i>
{{ event.temperature }}°F
</span>
{% endif %}
{% if event.blood_pressure %}
<span class="vital-item">
<i class="fa fa-heartbeat text-primary"></i>
{{ event.blood_pressure }}
</span>
{% endif %}
{% if event.heart_rate %}
<span class="vital-item">
<i class="fa fa-heart text-danger"></i>
{{ event.heart_rate }} bpm
</span>
{% endif %}
{% if event.oxygen_saturation %}
<span class="vital-item">
<i class="fa fa-lungs text-info"></i>
{{ event.oxygen_saturation }}%
</span>
{% endif %}
{% if event.has_critical_values %}
<span class="badge bg-danger ms-2">Critical Values</span>
{% endif %}
</div>
</div>
</div>
{% elif event.type == 'clinical_note' %}
<div class="note-preview">
<div class="small mb-2">
<strong>Note Type:</strong> {{ event.note_type_display }}
{% if event.electronically_signed %}
<i class="fa fa-check-circle text-success ms-2" title="Signed"></i>
{% endif %}
</div>
<div class="note-content">
{{ event.content|truncatewords:20 }}
{% if event.content|wordcount > 20 %}
<a href="{{ event.detail_url }}" class="text-primary">...read more</a>
{% endif %}
</div>
</div>
{% elif event.type == 'problem' %}
<div class="problem-summary">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>{{ event.problem_name }}</strong>
{% if event.problem_code %}
<span class="badge bg-secondary ms-2">{{ event.problem_code }}</span>
{% endif %}
</div>
<div>
<span class="badge bg-{{ event.severity_color }}">{{ event.severity_display }}</span>
<span class="badge bg-{{ event.status_color }}">{{ event.status_display }}</span>
</div>
</div>
{% if event.clinical_notes %}
<div class="small text-muted mt-2">
{{ event.clinical_notes|truncatechars:100 }}
</div>
{% endif %}
</div>
{% elif event.type == 'care_plan' %}
<div class="care-plan-summary">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>{{ event.title }}</strong>
<div class="small text-muted">{{ event.plan_type_display }}</div>
</div>
<div>
<div class="progress" style="width: 60px; height: 8px;">
<div class="progress-bar bg-{{ event.progress_color }}"
style="width: {{ event.completion_percentage }}%"></div>
</div>
<small class="text-muted">{{ event.completion_percentage }}%</small>
</div>
</div>
</div>
{% elif event.type == 'medication' %}
<div class="medication-summary">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>{{ event.medication_name }}</strong>
<div class="small text-muted">{{ event.dosage }} {{ event.frequency }}</div>
</div>
<div>
<span class="badge bg-{{ event.action_color }}">{{ event.action_display }}</span>
</div>
</div>
</div>
{% elif event.type == 'lab_result' %}
<div class="lab-result-summary">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>{{ event.test_name }}</strong>
<div class="small text-muted">{{ event.result_value }} {{ event.unit }}</div>
</div>
<div>
{% if event.is_abnormal %}
<span class="badge bg-warning">Abnormal</span>
{% else %}
<span class="badge bg-success">Normal</span>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="generic-event">
{% if event.description %}
<p class="mb-0">{{ event.description|truncatechars:150 }}</p>
{% endif %}
</div>
{% endif %}
</div>
<!-- Event Footer -->
{% if event.tags or event.priority %}
<div class="timeline-footer mt-2">
{% if event.tags %}
<div class="timeline-tags">
{% for tag in event.tags %}
<span class="badge bg-light text-dark me-1">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
{% if event.priority and event.priority != 'routine' %}
<span class="badge bg-{{ event.priority_color }} ms-2">{{ event.priority_display }}</span>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<!-- Load More Button -->
{% if has_more_events %}
<div class="text-center mt-3">
<button class="btn btn-outline-secondary" onclick="loadMoreEvents()">
<i class="fa fa-chevron-down me-2"></i>Load More Events
</button>
</div>
{% endif %}
<!-- Timeline Summary -->
<div class="timeline-summary mt-4 p-3 bg-light rounded">
<div class="row text-center">
<div class="col-md-2 col-4 mb-2">
<div class="fw-bold text-primary">{{ event_counts.encounters }}</div>
<div class="small text-muted">Encounters</div>
</div>
<div class="col-md-2 col-4 mb-2">
<div class="fw-bold text-success">{{ event_counts.vital_signs }}</div>
<div class="small text-muted">Vital Signs</div>
</div>
<div class="col-md-2 col-4 mb-2">
<div class="fw-bold text-info">{{ event_counts.clinical_notes }}</div>
<div class="small text-muted">Notes</div>
</div>
<div class="col-md-2 col-4 mb-2">
<div class="fw-bold text-warning">{{ event_counts.problems }}</div>
<div class="small text-muted">Problems</div>
</div>
<div class="col-md-2 col-4 mb-2">
<div class="fw-bold text-secondary">{{ event_counts.care_plans }}</div>
<div class="small text-muted">Care Plans</div>
</div>
<div class="col-md-2 col-4 mb-2">
<div class="fw-bold text-dark">{{ event_counts.total }}</div>
<div class="small text-muted">Total Events</div>
</div>
</div>
</div>
</div>
{% else %}
<!-- Empty State -->
<div class="text-center py-5">
<div class="mb-4">
<i class="fa fa-history fa-4x text-muted"></i>
</div>
<h6 class="text-muted mb-3">No Clinical Events</h6>
<p class="text-muted mb-4">
No clinical events have been recorded for this patient yet.<br>
Events will appear here as encounters, vital signs, and notes are documented.
</p>
<div class="btn-group">
<a href="{% url 'emr:encounter_create' %}" class="btn btn-primary btn-sm">
<i class="fa fa-plus me-2"></i>New Encounter
</a>
<a href="{% url 'emr:vital_signs_create' %}" class="btn btn-outline-secondary btn-sm">
<i class="fa fa-heartbeat me-2"></i>Record Vitals
</a>
</div>
</div>
{% endif %}
</div>
<script>
var currentFilter = 'all';
var currentPage = 1;
function filterTimeline(type) {
currentFilter = type;
currentPage = 1;
// Update button states
$('.clinical-timeline-widget .btn-group button').removeClass('active');
$('#filter-' + type).addClass('active');
// Show/hide timeline items
if (type === 'all') {
$('.timeline-item').show();
} else {
$('.timeline-item').hide();
$('.timeline-item[data-type="' + type + '"]').show();
}
// Update summary counts
updateTimelineSummary();
}
function loadTimelinePeriod() {
var period = $('#timeline-period').val();
var patientId = '{{ patient.id }}';
$.ajax({
url: '{% url "emr:clinical_timeline_api" %}',
data: {
'patient_id': patientId,
'period': period,
'filter': currentFilter,
'page': 1
},
success: function(data) {
$('#clinical-timeline').html(data.timeline_html);
updateTimelineSummary();
},
error: function() {
toastr.error('Failed to load timeline data');
}
});
}
function searchTimeline() {
var searchTerm = $('#timeline-search').val().toLowerCase();
if (searchTerm === '') {
$('.timeline-item').show();
} else {
$('.timeline-item').each(function() {
var text = $(this).text().toLowerCase();
if (text.includes(searchTerm)) {
$(this).show();
} else {
$(this).hide();
}
});
}
updateTimelineSummary();
}
function loadMoreEvents() {
currentPage++;
var period = $('#timeline-period').val();
var patientId = '{{ patient.id }}';
$.ajax({
url: '{% url "emr:clinical_timeline_api" %}',
data: {
'patient_id': patientId,
'period': period,
'filter': currentFilter,
'page': currentPage
},
success: function(data) {
$('#clinical-timeline').append(data.timeline_html);
if (!data.has_more) {
$('.load-more-button').hide();
}
},
error: function() {
toastr.error('Failed to load more events');
}
});
}
function updateTimelineSummary() {
var visibleItems = $('.timeline-item:visible');
var counts = {
encounters: 0,
vital_signs: 0,
clinical_notes: 0,
problems: 0,
care_plans: 0,
total: visibleItems.length
};
visibleItems.each(function() {
var type = $(this).data('type');
if (counts.hasOwnProperty(type)) {
counts[type]++;
}
});
// Update summary display
$('.timeline-summary .fw-bold').each(function(index) {
var types = ['encounters', 'vital_signs', 'clinical_notes', 'problems', 'care_plans', 'total'];
$(this).text(counts[types[index]] || 0);
});
}
// Auto-refresh timeline every 2 minutes
setInterval(function() {
if (currentFilter === 'all' && currentPage === 1) {
loadTimelinePeriod();
}
}, 120000);
// Initialize timeline
$(document).ready(function() {
// Set initial filter
$('#filter-all').addClass('active');
// Enable search on enter key
$('#timeline-search').on('keypress', function(e) {
if (e.which === 13) {
searchTimeline();
}
});
// Clear search
$('#timeline-search').on('input', function() {
if ($(this).val() === '') {
searchTimeline();
}
});
});
</script>
<style>
.clinical-timeline-widget .timeline {
position: relative;
padding-left: 0;
}
.clinical-timeline-widget .timeline-item {
position: relative;
padding-left: 60px;
margin-bottom: 30px;
border-left: 2px solid #e9ecef;
}
.clinical-timeline-widget .timeline-item:last-child {
border-left: 2px solid transparent;
}
.clinical-timeline-widget .timeline-time {
position: absolute;
left: -55px;
top: 0;
text-align: center;
width: 50px;
}
.clinical-timeline-widget .timeline-date {
font-size: 11px;
font-weight: 600;
color: #6c757d;
line-height: 1;
}
.clinical-timeline-widget .timeline-hour {
font-size: 10px;
color: #adb5bd;
line-height: 1;
}
.clinical-timeline-widget .timeline-icon {
position: absolute;
left: -11px;
top: 0;
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: white;
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.clinical-timeline-widget .timeline-body {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: box-shadow 0.2s ease;
}
.clinical-timeline-widget .timeline-body:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.clinical-timeline-widget .timeline-title {
font-size: 14px;
font-weight: 600;
color: #495057;
margin-bottom: 5px;
}
.clinical-timeline-widget .timeline-meta {
margin-bottom: 10px;
}
.clinical-timeline-widget .timeline-content {
font-size: 13px;
color: #6c757d;
}
.clinical-timeline-widget .vital-signs-summary .vital-item {
display: inline-block;
margin-right: 15px;
margin-bottom: 5px;
font-size: 12px;
}
.clinical-timeline-widget .vital-signs-summary .vital-item i {
margin-right: 3px;
}
.clinical-timeline-widget .note-content {
font-size: 12px;
line-height: 1.4;
color: #495057;
}
.clinical-timeline-widget .timeline-actions .btn {
padding: 2px 6px;
font-size: 10px;
}
.clinical-timeline-widget .timeline-footer {
border-top: 1px solid #f8f9fa;
padding-top: 10px;
}
.clinical-timeline-widget .timeline-tags .badge {
font-size: 10px;
}
.clinical-timeline-widget .timeline-summary {
border: 1px solid #e9ecef;
}
/* Type-specific colors */
.bg-encounter { background-color: #0d6efd !important; }
.bg-vital_signs { background-color: #dc3545 !important; }
.bg-clinical_note { background-color: #198754 !important; }
.bg-problem { background-color: #fd7e14 !important; }
.bg-care_plan { background-color: #6f42c1 !important; }
.bg-medication { background-color: #20c997 !important; }
.bg-lab_result { background-color: #ffc107 !important; }
/* Status colors */
.bg-active { background-color: #198754 !important; }
.bg-inactive { background-color: #6c757d !important; }
.bg-completed { background-color: #0d6efd !important; }
.bg-cancelled { background-color: #dc3545 !important; }
/* Severity colors */
.bg-mild { background-color: #198754 !important; }
.bg-moderate { background-color: #ffc107 !important; }
.bg-severe { background-color: #fd7e14 !important; }
.bg-critical { background-color: #dc3545 !important; }
/* Priority colors */
.bg-low { background-color: #6c757d !important; }
.bg-medium { background-color: #0d6efd !important; }
.bg-high { background-color: #fd7e14 !important; }
.bg-urgent { background-color: #dc3545 !important; }
/* Progress colors */
.bg-progress-low { background-color: #dc3545 !important; }
.bg-progress-medium { background-color: #ffc107 !important; }
.bg-progress-high { background-color: #198754 !important; }
/* Responsive adjustments */
@media (max-width: 768px) {
.clinical-timeline-widget .timeline-item {
padding-left: 40px;
}
.clinical-timeline-widget .timeline-time {
left: -35px;
width: 30px;
}
.clinical-timeline-widget .timeline-icon {
left: -8px;
width: 16px;
height: 16px;
font-size: 8px;
}
.clinical-timeline-widget .timeline-body {
padding: 10px;
}
.clinical-timeline-widget .btn-group {
flex-direction: column;
width: 100%;
}
.clinical-timeline-widget .btn-group button {
margin-bottom: 2px;
}
}
</style>