643 lines
24 KiB
HTML
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>
|
|
|