629 lines
26 KiB
HTML
629 lines
26 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Series {{ series.series_number }} - {{ series.series_description|default:"Imaging Series" }} - Hospital Management{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="content">
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="page-header">
|
|
<div class="page-title">
|
|
<h4>Imaging Series Details</h4>
|
|
<h6>Series {{ series.series_number }} - {{ series.series_description|default:"No Description" }}</h6>
|
|
</div>
|
|
<div class="page-btn">
|
|
<a href="{% url 'radiology:imaging_series_list' series.study.pk %}" class="btn btn-secondary me-2">
|
|
<i class="fas fa-arrow-left me-1"></i>Back to Series List
|
|
</a>
|
|
<a href="{% url 'radiology:imaging_series_edit' series.pk %}" class="btn btn-primary me-2">
|
|
<i class="fas fa-edit me-1"></i>Edit Series
|
|
</a>
|
|
<div class="btn-group">
|
|
<button class="btn btn-success dropdown-toggle" data-bs-toggle="dropdown">
|
|
<i class="fas fa-download me-1"></i>Download
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li>
|
|
<a class="dropdown-item" href="{% url 'radiology:imaging_series_download' series.pk %}">
|
|
<i class="fas fa-file-medical me-2"></i>DICOM Files
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="dropdown-item" href="#" onclick="exportSeries('{{ series.pk }}', 'pdf')">
|
|
<i class="fas fa-file-pdf me-2"></i>PDF Report
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="dropdown-item" href="#" onclick="exportSeries('{{ series.pk }}', 'zip')">
|
|
<i class="fas fa-file-archive me-2"></i>ZIP Archive
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Series Information -->
|
|
<div class="row mb-4">
|
|
<div class="col-lg-8">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-info-circle me-2"></i>Series Information
|
|
</h5>
|
|
<div class="card-tools">
|
|
<span class="badge bg-{{ series.get_status_color|default:'success' }} fs-6">
|
|
{{ series.get_status_display|default:'Completed' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="info-group">
|
|
<label class="form-label">Series Number:</label>
|
|
<p class="fw-bold">{{ series.series_number }}</p>
|
|
</div>
|
|
<div class="info-group">
|
|
<label class="form-label">Series Description:</label>
|
|
<p>{{ series.series_description|default:"No description provided" }}</p>
|
|
</div>
|
|
<div class="info-group">
|
|
<label class="form-label">Protocol Name:</label>
|
|
<p>{{ series.protocol_name|default:"N/A" }}</p>
|
|
</div>
|
|
<div class="info-group">
|
|
<label class="form-label">Modality:</label>
|
|
<p><span class="badge bg-primary">{{ series.get_modality_display }}</span></p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="info-group">
|
|
<label class="form-label">Series Instance UID:</label>
|
|
<p class="text-monospace small">{{ series.series_instance_uid }}</p>
|
|
</div>
|
|
<div class="info-group">
|
|
<label class="form-label">Series Date:</label>
|
|
<p>{{ series.series_date|date:"F d, Y" }}</p>
|
|
</div>
|
|
<div class="info-group">
|
|
<label class="form-label">Series Time:</label>
|
|
<p>{{ series.series_time|time:"H:i:s" }}</p>
|
|
</div>
|
|
<div class="info-group">
|
|
<label class="form-label">Number of Images:</label>
|
|
<p class="fw-bold">{{ series.number_of_images|default:0 }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-chart-bar me-2"></i>Series Statistics
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="stat-item">
|
|
<div class="stat-icon">
|
|
<i class="fas fa-images text-primary"></i>
|
|
</div>
|
|
<div class="stat-content">
|
|
<h6>{{ series.number_of_images|default:0 }}</h6>
|
|
<p>Total Images</p>
|
|
</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-icon">
|
|
<i class="fas fa-hdd text-info"></i>
|
|
</div>
|
|
<div class="stat-content">
|
|
<h6>{{ series.total_size|default:"N/A" }}</h6>
|
|
<p>Total Size</p>
|
|
</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-icon">
|
|
<i class="fas fa-clock text-warning"></i>
|
|
</div>
|
|
<div class="stat-content">
|
|
<h6>{{ series.acquisition_duration|default:"N/A" }}</h6>
|
|
<p>Acquisition Time</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Study Context -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-folder-open me-2"></i>Study Context
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<div class="info-group">
|
|
<label class="form-label">Patient:</label>
|
|
<p class="fw-bold">{{ series.study.patient.get_full_name }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="info-group">
|
|
<label class="form-label">Study Description:</label>
|
|
<p>{{ series.study.study_description }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="info-group">
|
|
<label class="form-label">Accession Number:</label>
|
|
<p>{{ series.study.accession_number }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="info-group">
|
|
<label class="form-label">Referring Physician:</label>
|
|
<p>{{ series.study.referring_physician.get_full_name }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Technical Parameters -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-cogs me-2"></i>Technical Parameters
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<div class="info-group">
|
|
<label class="form-label">Slice Thickness:</label>
|
|
<p>{{ series.slice_thickness|default:"N/A" }}{% if series.slice_thickness %} mm{% endif %}</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="info-group">
|
|
<label class="form-label">Spacing Between Slices:</label>
|
|
<p>{{ series.spacing_between_slices|default:"N/A" }}{% if series.spacing_between_slices %} mm{% endif %}</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="info-group">
|
|
<label class="form-label">Pixel Spacing:</label>
|
|
<p>{{ series.pixel_spacing|default:"N/A" }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="info-group">
|
|
<label class="form-label">Image Orientation:</label>
|
|
<p>{{ series.image_orientation|default:"N/A" }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% if series.acquisition_parameters %}
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<div class="info-group">
|
|
<label class="form-label">Additional Parameters:</label>
|
|
<div class="parameter-list">
|
|
{% for key, value in series.acquisition_parameters.items %}
|
|
<div class="parameter-item">
|
|
<span class="parameter-key">{{ key }}:</span>
|
|
<span class="parameter-value">{{ value }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image Viewer -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-images me-2"></i>Image Viewer
|
|
</h5>
|
|
<div class="card-tools">
|
|
<button class="btn btn-outline-primary btn-sm" onclick="openFullViewer()">
|
|
<i class="fas fa-expand me-1"></i>Full Screen Viewer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="image-viewer-container" class="text-center">
|
|
{% if series.number_of_images > 0 %}
|
|
<div class="image-viewer">
|
|
<div class="viewer-controls mb-3">
|
|
<div class="btn-group me-3">
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="previousImage()">
|
|
<i class="fas fa-chevron-left"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="playPause()">
|
|
<i class="fas fa-play" id="play-icon"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="nextImage()">
|
|
<i class="fas fa-chevron-right"></i>
|
|
</button>
|
|
</div>
|
|
<div class="btn-group me-3">
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="zoomIn()">
|
|
<i class="fas fa-search-plus"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="zoomOut()">
|
|
<i class="fas fa-search-minus"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="resetZoom()">
|
|
<i class="fas fa-expand-arrows-alt"></i>
|
|
</button>
|
|
</div>
|
|
<div class="image-counter">
|
|
<span id="current-image">1</span> / <span id="total-images">{{ series.number_of_images }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="image-display">
|
|
<div id="dicom-viewer" style="width: 100%; height: 400px; background: #000; border: 1px solid #ddd;">
|
|
<!-- DICOM viewer will be initialized here -->
|
|
<div class="d-flex align-items-center justify-content-center h-100 text-white">
|
|
<div class="text-center">
|
|
<i class="fas fa-image fa-3x mb-3"></i>
|
|
<p>DICOM Viewer Loading...</p>
|
|
<button class="btn btn-primary" onclick="initializeViewer()">
|
|
<i class="fas fa-play me-1"></i>Initialize Viewer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="image-slider mt-3">
|
|
<input type="range" class="form-range" id="image-slider"
|
|
min="1" max="{{ series.number_of_images }}" value="1"
|
|
onchange="goToImage(this.value)">
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="no-images py-5">
|
|
<i class="fas fa-image fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No Images Available</h5>
|
|
<p class="text-muted">This series does not contain any images yet.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Related Series -->
|
|
{% if related_series %}
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-layer-group me-2"></i>Related Series in Study
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
{% for related in related_series %}
|
|
<div class="col-lg-4 col-md-6 mb-3">
|
|
<div class="card border {% if related.pk == series.pk %}border-primary{% endif %}">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
<h6 class="card-title mb-0">Series {{ related.series_number }}</h6>
|
|
{% if related.pk == series.pk %}
|
|
<span class="badge bg-primary">Current</span>
|
|
{% endif %}
|
|
</div>
|
|
<p class="card-text small text-muted">
|
|
{{ related.series_description|default:"No description" }}
|
|
</p>
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<small class="text-muted">
|
|
<span class="badge bg-secondary">{{ related.get_modality_display }}</span>
|
|
</small>
|
|
{% if related.pk != series.pk %}
|
|
<a href="{% url 'radiology:imaging_series_detail' related.pk %}"
|
|
class="btn btn-outline-primary btn-sm">
|
|
<i class="fas fa-eye me-1"></i>View
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div class="action-buttons">
|
|
<a href="{% url 'radiology:imaging_series_edit' series.pk %}" class="btn btn-primary me-2">
|
|
<i class="fas fa-edit me-1"></i>Edit Series
|
|
</a>
|
|
<button class="btn btn-info me-2" onclick="openFullViewer()">
|
|
<i class="fas fa-expand me-1"></i>Full Screen Viewer
|
|
</button>
|
|
<button class="btn btn-success me-2" onclick="downloadSeries()">
|
|
<i class="fas fa-download me-1"></i>Download DICOM
|
|
</button>
|
|
</div>
|
|
<div class="danger-actions">
|
|
<a href="{% url 'radiology:imaging_series_delete' series.pk %}"
|
|
class="btn btn-outline-danger"
|
|
onclick="return confirm('Are you sure you want to delete this series? This action cannot be undone.')">
|
|
<i class="fas fa-trash me-1"></i>Delete Series
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentImageIndex = 1;
|
|
let totalImages = {{ series.number_of_images|default:0 }};
|
|
let isPlaying = false;
|
|
let playInterval;
|
|
|
|
$(document).ready(function() {
|
|
// Initialize viewer if images are available
|
|
if (totalImages > 0) {
|
|
initializeViewer();
|
|
}
|
|
});
|
|
|
|
function initializeViewer() {
|
|
// Initialize DICOM viewer
|
|
// This would integrate with a DICOM viewer library like Cornerstone.js
|
|
console.log('Initializing DICOM viewer for series: {{ series.series_instance_uid }}');
|
|
|
|
// Placeholder for DICOM viewer initialization
|
|
$('#dicom-viewer').html(`
|
|
<div class="d-flex align-items-center justify-content-center h-100 text-white">
|
|
<div class="text-center">
|
|
<i class="fas fa-check-circle fa-3x mb-3 text-success"></i>
|
|
<p>DICOM Viewer Initialized</p>
|
|
<p class="small">Series: {{ series.series_instance_uid }}</p>
|
|
</div>
|
|
</div>
|
|
`);
|
|
}
|
|
|
|
function previousImage() {
|
|
if (currentImageIndex > 1) {
|
|
currentImageIndex--;
|
|
updateImageDisplay();
|
|
}
|
|
}
|
|
|
|
function nextImage() {
|
|
if (currentImageIndex < totalImages) {
|
|
currentImageIndex++;
|
|
updateImageDisplay();
|
|
}
|
|
}
|
|
|
|
function goToImage(index) {
|
|
currentImageIndex = parseInt(index);
|
|
updateImageDisplay();
|
|
}
|
|
|
|
function updateImageDisplay() {
|
|
$('#current-image').text(currentImageIndex);
|
|
$('#image-slider').val(currentImageIndex);
|
|
|
|
// Update DICOM viewer to show current image
|
|
console.log('Displaying image:', currentImageIndex);
|
|
}
|
|
|
|
function playPause() {
|
|
if (isPlaying) {
|
|
clearInterval(playInterval);
|
|
$('#play-icon').removeClass('fa-pause').addClass('fa-play');
|
|
isPlaying = false;
|
|
} else {
|
|
playInterval = setInterval(function() {
|
|
if (currentImageIndex < totalImages) {
|
|
nextImage();
|
|
} else {
|
|
currentImageIndex = 1;
|
|
updateImageDisplay();
|
|
}
|
|
}, 500);
|
|
$('#play-icon').removeClass('fa-play').addClass('fa-pause');
|
|
isPlaying = true;
|
|
}
|
|
}
|
|
|
|
function zoomIn() {
|
|
// Implement zoom in functionality
|
|
console.log('Zoom in');
|
|
}
|
|
|
|
function zoomOut() {
|
|
// Implement zoom out functionality
|
|
console.log('Zoom out');
|
|
}
|
|
|
|
function resetZoom() {
|
|
// Implement reset zoom functionality
|
|
console.log('Reset zoom');
|
|
}
|
|
|
|
function openFullViewer() {
|
|
// Open full screen DICOM viewer
|
|
window.open(`/radiology/series/{{ series.pk }}/viewer/`, '_blank');
|
|
}
|
|
|
|
function downloadSeries() {
|
|
// Download series as DICOM files
|
|
window.location.href = `{% url 'radiology:imaging_series_download' series.pk %}`;
|
|
}
|
|
|
|
function exportSeries(seriesId, format) {
|
|
// Export series in specified format
|
|
if (confirm(`Export series as ${format.toUpperCase()}?`)) {
|
|
window.location.href = `/radiology/series/${seriesId}/export/${format}/`;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.info-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.info-group .form-label {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.info-group p {
|
|
margin-bottom: 0;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.stat-item {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
padding: 10px;
|
|
background: #f8f9fa;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.stat-icon {
|
|
margin-right: 15px;
|
|
font-size: 24px;
|
|
}
|
|
|
|
.stat-content h6 {
|
|
margin-bottom: 0;
|
|
font-weight: bold;
|
|
color: #495057;
|
|
}
|
|
|
|
.stat-content p {
|
|
margin-bottom: 0;
|
|
color: #6c757d;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.parameter-list {
|
|
background: #f8f9fa;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.parameter-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 8px;
|
|
padding: 4px 0;
|
|
border-bottom: 1px solid #e9ecef;
|
|
}
|
|
|
|
.parameter-item:last-child {
|
|
border-bottom: none;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.parameter-key {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.parameter-value {
|
|
color: #6c757d;
|
|
}
|
|
|
|
.viewer-controls {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 15px;
|
|
}
|
|
|
|
.image-counter {
|
|
font-weight: bold;
|
|
color: #495057;
|
|
}
|
|
|
|
.image-display {
|
|
position: relative;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.action-buttons .btn {
|
|
margin-right: 10px;
|
|
}
|
|
|
|
/* Mobile Responsive */
|
|
@media (max-width: 768px) {
|
|
.viewer-controls {
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.action-buttons .btn {
|
|
margin-right: 0;
|
|
}
|
|
|
|
.d-flex.justify-content-between {
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|