hospital-management/templates/radiology/dicom/dicom_metadata_editor.html
Marwan Alwali 0a037d3d9d update
2025-09-01 11:26:11 +03:00

1298 lines
48 KiB
HTML

{% extends 'base.html' %}
{% load static %}
{% block title %}DICOM Metadata Editor - {{ file.filename }}{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<style>
.editor-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0.5rem;
padding: 2rem;
margin-bottom: 2rem;
}
.editor-layout {
display: grid;
grid-template-columns: 1fr 400px;
gap: 2rem;
margin-bottom: 2rem;
}
.editor-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
overflow: hidden;
}
.section-header {
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 1rem 1.5rem;
font-weight: 600;
color: #495057;
display: flex;
justify-content: between;
align-items: center;
}
.section-content {
padding: 1.5rem;
}
.metadata-tabs {
border-bottom: 1px solid #dee2e6;
margin-bottom: 1.5rem;
}
.nav-tabs .nav-link {
border: none;
border-bottom: 2px solid transparent;
color: #6c757d;
font-weight: 600;
padding: 1rem 1.5rem;
}
.nav-tabs .nav-link.active {
color: #007bff;
border-bottom-color: #007bff;
background: none;
}
.metadata-group {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1.5rem;
}
.metadata-group h6 {
color: #495057;
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #dee2e6;
}
.tag-row {
display: grid;
grid-template-columns: 120px 200px 1fr auto;
gap: 1rem;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid #f1f3f4;
}
.tag-row:last-child {
border-bottom: none;
}
.tag-id {
font-family: 'Courier New', monospace;
font-size: 0.875rem;
color: #6c757d;
background: #f8f9fa;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.tag-name {
font-weight: 600;
color: #495057;
font-size: 0.875rem;
}
.tag-value {
flex: 1;
}
.tag-value input, .tag-value select, .tag-value textarea {
width: 100%;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 0.5rem;
font-size: 0.875rem;
}
.tag-value textarea {
resize: vertical;
min-height: 60px;
}
.tag-actions {
display: flex;
gap: 0.25rem;
}
.btn-tag {
padding: 0.25rem 0.5rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.75rem;
}
.btn-edit { background: #fff3e0; color: #f57c00; }
.btn-save { background: #e8f5e8; color: #2e7d32; }
.btn-cancel { background: #ffebee; color: #d32f2f; }
.btn-reset { background: #f3e5f5; color: #7b1fa2; }
.preview-panel {
position: sticky;
top: 2rem;
height: fit-content;
}
.image-preview {
width: 100%;
max-height: 300px;
object-fit: contain;
border-radius: 0.375rem;
background: #f8f9fa;
border: 1px solid #dee2e6;
}
.file-info {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1.5rem;
}
.info-row {
display: flex;
justify-content: between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #dee2e6;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-weight: 600;
color: #6c757d;
font-size: 0.875rem;
}
.info-value {
color: #495057;
font-size: 0.875rem;
}
.validation-errors {
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1.5rem;
display: none;
}
.validation-errors.show {
display: block;
}
.error-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0;
color: #721c24;
font-size: 0.875rem;
}
.changes-summary {
background: #d1ecf1;
border: 1px solid #bee5eb;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1.5rem;
display: none;
}
.changes-summary.show {
display: block;
}
.change-item {
display: flex;
justify-content: between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #bee5eb;
font-size: 0.875rem;
}
.change-item:last-child {
border-bottom: none;
}
.change-tag {
font-weight: 600;
color: #0c5460;
}
.change-values {
display: flex;
align-items: center;
gap: 0.5rem;
color: #6c757d;
}
.old-value {
text-decoration: line-through;
color: #dc3545;
}
.new-value {
color: #28a745;
font-weight: 600;
}
.search-tags {
position: relative;
margin-bottom: 1.5rem;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.5rem;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
font-size: 0.875rem;
}
.search-icon {
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
color: #6c757d;
}
.tag-filter {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.filter-btn {
padding: 0.5rem 1rem;
border: 1px solid #dee2e6;
background: white;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
color: #495057;
}
.filter-btn:hover, .filter-btn.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.modified-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ffc107;
display: inline-block;
margin-left: 0.5rem;
}
.required-field {
position: relative;
}
.required-field::after {
content: "*";
color: #dc3545;
position: absolute;
right: -10px;
top: 0;
}
.auto-save-indicator {
position: fixed;
top: 20px;
right: 20px;
background: #28a745;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
font-size: 0.875rem;
z-index: 1050;
display: none;
}
@media (max-width: 1200px) {
.editor-layout {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.preview-panel {
position: static;
}
}
@media (max-width: 768px) {
.editor-header {
padding: 1.5rem;
}
.section-content {
padding: 1rem;
}
.tag-row {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.tag-filter {
justify-content: center;
}
}
@media print {
.preview-panel, .tag-actions, .btn {
display: none !important;
}
.section-header {
background: none;
border-bottom: 2px solid #000;
color: #000;
}
.editor-header {
background: none;
color: #000;
border: 2px solid #000;
}
}
</style>
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<!-- Auto-save indicator -->
<div class="auto-save-indicator" id="auto-save-indicator">
<i class="fas fa-check me-1"></i>Auto-saved
</div>
<!-- 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 'radiology:dashboard' %}">Radiology</a></li>
<li class="breadcrumb-item"><a href="{% url 'radiology:dicom_file_list' %}">DICOM Files</a></li>
<li class="breadcrumb-item active">Metadata Editor</li>
</ol>
<h1 class="page-header mb-0">
<i class="fas fa-edit me-2"></i>DICOM Metadata Editor
</h1>
</div>
<div class="ms-auto">
<button type="button" class="btn btn-outline-secondary me-2" onclick="resetChanges()">
<i class="fas fa-undo me-1"></i>Reset
</button>
<button type="button" class="btn btn-outline-info me-2" onclick="validateMetadata()">
<i class="fas fa-check-circle me-1"></i>Validate
</button>
<button type="button" class="btn btn-outline-warning me-2" onclick="exportMetadata()">
<i class="fas fa-download me-1"></i>Export
</button>
<button type="button" class="btn btn-success" onclick="saveChanges()">
<i class="fas fa-save me-1"></i>Save Changes
</button>
</div>
</div>
<!-- Editor Header -->
<div class="editor-header">
<div class="row align-items-center">
<div class="col-md-8">
<h3 class="mb-2">{{ file.filename }}</h3>
<p class="mb-2">{{ file.patient_name|default:"Unknown Patient" }} - {{ file.study_description|default:"No description" }}</p>
<div class="d-flex align-items-center gap-3">
<span class="badge bg-primary">{{ file.modality|default:"Unknown" }}</span>
<span class="badge bg-info">{{ file.file_size|filesizeformat }}</span>
<span class="badge bg-success">{{ file.get_status_display }}</span>
</div>
</div>
<div class="col-md-4 text-md-end">
<div class="text-white-50 mb-1">Last Modified</div>
<div class="h6 mb-2">{{ file.modified_at|date:"M d, Y g:i A" }}</div>
<div class="text-white-50">by {{ file.modified_by.get_full_name|default:"System" }}</div>
</div>
</div>
</div>
<!-- Validation Errors -->
<div class="validation-errors" id="validation-errors">
<h6 class="text-danger mb-3">
<i class="fas fa-exclamation-triangle me-2"></i>Validation Errors
</h6>
<div id="error-list">
<!-- Errors will be populated here -->
</div>
</div>
<!-- Changes Summary -->
<div class="changes-summary" id="changes-summary">
<h6 class="text-info mb-3">
<i class="fas fa-edit me-2"></i>Pending Changes (<span id="changes-count">0</span>)
</h6>
<div id="changes-list">
<!-- Changes will be populated here -->
</div>
</div>
<div class="editor-layout">
<!-- Main Editor -->
<div class="editor-section">
<div class="section-header">
<div>
<i class="fas fa-tags me-2"></i>DICOM Metadata
</div>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-secondary" id="total-tags">{{ total_tags }} tags</span>
<span class="badge bg-warning" id="modified-tags">0 modified</span>
</div>
</div>
<div class="section-content">
<!-- Search and Filter -->
<div class="search-tags">
<i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" id="tag-search"
placeholder="Search tags by name, ID, or value...">
</div>
<div class="tag-filter">
<button type="button" class="filter-btn active" data-filter="all">All Tags</button>
<button type="button" class="filter-btn" data-filter="patient">Patient Info</button>
<button type="button" class="filter-btn" data-filter="study">Study Info</button>
<button type="button" class="filter-btn" data-filter="series">Series Info</button>
<button type="button" class="filter-btn" data-filter="image">Image Info</button>
<button type="button" class="filter-btn" data-filter="modified">Modified</button>
<button type="button" class="filter-btn" data-filter="required">Required</button>
</div>
<!-- Metadata Tabs -->
<ul class="nav nav-tabs metadata-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#patient-tab">
<i class="fas fa-user me-1"></i>Patient
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#study-tab">
<i class="fas fa-folder me-1"></i>Study
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#series-tab">
<i class="fas fa-layer-group me-1"></i>Series
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#image-tab">
<i class="fas fa-image me-1"></i>Image
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#technical-tab">
<i class="fas fa-cog me-1"></i>Technical
</a>
</li>
</ul>
<div class="tab-content">
<!-- Patient Information Tab -->
<div class="tab-pane fade show active" id="patient-tab">
<div class="metadata-group">
<h6>Patient Demographics</h6>
<div class="tag-row">
<div class="tag-id">(0010,0010)</div>
<div class="tag-name required-field">Patient Name</div>
<div class="tag-value">
<input type="text" id="tag-0010-0010" value="{{ file.patient_name|default:'' }}"
onchange="markModified(this)" data-original="{{ file.patient_name|default:'' }}">
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0010-0010')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0010,0020)</div>
<div class="tag-name required-field">Patient ID</div>
<div class="tag-value">
<input type="text" id="tag-0010-0020" value="{{ file.patient_id|default:'' }}"
onchange="markModified(this)" data-original="{{ file.patient_id|default:'' }}">
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0010-0020')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0010,0030)</div>
<div class="tag-name">Patient Birth Date</div>
<div class="tag-value">
<input type="date" id="tag-0010-0030" value="{{ file.patient_birth_date|date:'Y-m-d'|default:'' }}"
onchange="markModified(this)" data-original="{{ file.patient_birth_date|date:'Y-m-d'|default:'' }}">
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0010-0030')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0010,0040)</div>
<div class="tag-name">Patient Sex</div>
<div class="tag-value">
<select id="tag-0010-0040" onchange="markModified(this)" data-original="{{ file.patient_sex|default:'' }}">
<option value="">Unknown</option>
<option value="M" {% if file.patient_sex == 'M' %}selected{% endif %}>Male</option>
<option value="F" {% if file.patient_sex == 'F' %}selected{% endif %}>Female</option>
<option value="O" {% if file.patient_sex == 'O' %}selected{% endif %}>Other</option>
</select>
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0010-0040')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Study Information Tab -->
<div class="tab-pane fade" id="study-tab">
<div class="metadata-group">
<h6>Study Details</h6>
<div class="tag-row">
<div class="tag-id">(0020,000D)</div>
<div class="tag-name required-field">Study Instance UID</div>
<div class="tag-value">
<input type="text" id="tag-0020-000D" value="{{ file.study_instance_uid|default:'' }}"
onchange="markModified(this)" data-original="{{ file.study_instance_uid|default:'' }}" readonly>
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-edit" onclick="generateUID('tag-0020-000D')">
<i class="fas fa-sync"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0008,0020)</div>
<div class="tag-name">Study Date</div>
<div class="tag-value">
<input type="date" id="tag-0008-0020" value="{{ file.study_date|date:'Y-m-d'|default:'' }}"
onchange="markModified(this)" data-original="{{ file.study_date|date:'Y-m-d'|default:'' }}">
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0008-0020')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0008,0030)</div>
<div class="tag-name">Study Time</div>
<div class="tag-value">
<input type="time" id="tag-0008-0030" value="{{ file.study_time|time:'H:i'|default:'' }}"
onchange="markModified(this)" data-original="{{ file.study_time|time:'H:i'|default:'' }}">
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0008-0030')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0008,1030)</div>
<div class="tag-name">Study Description</div>
<div class="tag-value">
<textarea id="tag-0008-1030" onchange="markModified(this)"
data-original="{{ file.study_description|default:'' }}">{{ file.study_description|default:'' }}</textarea>
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0008-1030')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Series Information Tab -->
<div class="tab-pane fade" id="series-tab">
<div class="metadata-group">
<h6>Series Details</h6>
<div class="tag-row">
<div class="tag-id">(0020,000E)</div>
<div class="tag-name required-field">Series Instance UID</div>
<div class="tag-value">
<input type="text" id="tag-0020-000E" value="{{ file.series_instance_uid|default:'' }}"
onchange="markModified(this)" data-original="{{ file.series_instance_uid|default:'' }}" readonly>
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-edit" onclick="generateUID('tag-0020-000E')">
<i class="fas fa-sync"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0020,0011)</div>
<div class="tag-name">Series Number</div>
<div class="tag-value">
<input type="number" id="tag-0020-0011" value="{{ file.series_number|default:'' }}"
onchange="markModified(this)" data-original="{{ file.series_number|default:'' }}">
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0020-0011')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0008,103E)</div>
<div class="tag-name">Series Description</div>
<div class="tag-value">
<textarea id="tag-0008-103E" onchange="markModified(this)"
data-original="{{ file.series_description|default:'' }}">{{ file.series_description|default:'' }}</textarea>
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0008-103E')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0008,0060)</div>
<div class="tag-name required-field">Modality</div>
<div class="tag-value">
<select id="tag-0008-0060" onchange="markModified(this)" data-original="{{ file.modality|default:'' }}">
<option value="">Select Modality</option>
<option value="CT" {% if file.modality == 'CT' %}selected{% endif %}>CT</option>
<option value="MRI" {% if file.modality == 'MRI' %}selected{% endif %}>MRI</option>
<option value="XR" {% if file.modality == 'XR' %}selected{% endif %}>X-Ray</option>
<option value="US" {% if file.modality == 'US' %}selected{% endif %}>Ultrasound</option>
<option value="CR" {% if file.modality == 'CR' %}selected{% endif %}>CR</option>
<option value="DX" {% if file.modality == 'DX' %}selected{% endif %}>DX</option>
</select>
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0008-0060')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Image Information Tab -->
<div class="tab-pane fade" id="image-tab">
<div class="metadata-group">
<h6>Image Properties</h6>
<div class="tag-row">
<div class="tag-id">(0020,0013)</div>
<div class="tag-name">Instance Number</div>
<div class="tag-value">
<input type="number" id="tag-0020-0013" value="{{ file.instance_number|default:'' }}"
onchange="markModified(this)" data-original="{{ file.instance_number|default:'' }}">
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0020-0013')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0020,1041)</div>
<div class="tag-name">Slice Location</div>
<div class="tag-value">
<input type="number" step="0.01" id="tag-0020-1041" value="{{ file.slice_location|default:'' }}"
onchange="markModified(this)" data-original="{{ file.slice_location|default:'' }}">
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0020-1041')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0028,0010)</div>
<div class="tag-name">Rows</div>
<div class="tag-value">
<input type="number" id="tag-0028-0010" value="{{ file.rows|default:'' }}"
onchange="markModified(this)" data-original="{{ file.rows|default:'' }}">
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0028-0010')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0028,0011)</div>
<div class="tag-name">Columns</div>
<div class="tag-value">
<input type="number" id="tag-0028-0011" value="{{ file.columns|default:'' }}"
onchange="markModified(this)" data-original="{{ file.columns|default:'' }}">
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0028-0011')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Technical Information Tab -->
<div class="tab-pane fade" id="technical-tab">
<div class="metadata-group">
<h6>Technical Parameters</h6>
<div class="tag-row">
<div class="tag-id">(0008,0008)</div>
<div class="tag-name">Image Type</div>
<div class="tag-value">
<input type="text" id="tag-0008-0008" value="{{ file.image_type|default:'' }}"
onchange="markModified(this)" data-original="{{ file.image_type|default:'' }}">
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0008-0008')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0008,0070)</div>
<div class="tag-name">Manufacturer</div>
<div class="tag-value">
<input type="text" id="tag-0008-0070" value="{{ file.manufacturer|default:'' }}"
onchange="markModified(this)" data-original="{{ file.manufacturer|default:'' }}">
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0008-0070')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
<div class="tag-row">
<div class="tag-id">(0008,1090)</div>
<div class="tag-name">Manufacturer Model</div>
<div class="tag-value">
<input type="text" id="tag-0008-1090" value="{{ file.manufacturer_model|default:'' }}"
onchange="markModified(this)" data-original="{{ file.manufacturer_model|default:'' }}">
</div>
<div class="tag-actions">
<button type="button" class="btn-tag btn-reset" onclick="resetTag('tag-0008-1090')">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Preview Panel -->
<div class="preview-panel">
<!-- File Information -->
<div class="editor-section">
<div class="section-header">
<i class="fas fa-info-circle me-2"></i>File Information
</div>
<div class="section-content">
<div class="file-info">
<div class="info-row">
<div class="info-label">Filename:</div>
<div class="info-value">{{ file.filename }}</div>
</div>
<div class="info-row">
<div class="info-label">Size:</div>
<div class="info-value">{{ file.file_size|filesizeformat }}</div>
</div>
<div class="info-row">
<div class="info-label">Upload Date:</div>
<div class="info-value">{{ file.uploaded_at|date:"M d, Y" }}</div>
</div>
<div class="info-row">
<div class="info-label">Status:</div>
<div class="info-value">{{ file.get_status_display }}</div>
</div>
</div>
{% if file.thumbnail %}
<div class="text-center">
<img src="{{ file.thumbnail.url }}" alt="DICOM Preview" class="image-preview">
</div>
{% endif %}
</div>
</div>
<!-- Quick Actions -->
<div class="editor-section">
<div class="section-header">
<i class="fas fa-bolt me-2"></i>Quick Actions
</div>
<div class="section-content">
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-primary" onclick="openViewer()">
<i class="fas fa-eye me-1"></i>Open in Viewer
</button>
<button type="button" class="btn btn-outline-info" onclick="downloadFile()">
<i class="fas fa-download me-1"></i>Download File
</button>
<button type="button" class="btn btn-outline-success" onclick="validateDICOM()">
<i class="fas fa-check-circle me-1"></i>Validate DICOM
</button>
<button type="button" class="btn btn-outline-warning" onclick="anonymizeFile()">
<i class="fas fa-user-secret me-1"></i>Anonymize
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script>
let modifiedTags = new Set();
let originalValues = {};
$(document).ready(function() {
// Initialize Select2
$('.tag-value select').select2({
theme: 'bootstrap-5',
width: '100%'
});
// Store original values
$('input, select, textarea').each(function() {
const id = this.id;
const value = $(this).val();
originalValues[id] = value;
});
// Auto-save functionality
let autoSaveTimer;
$('input, textarea, select').on('input change', function() {
scheduleAutoSave();
});
// Tag search functionality
$('#tag-search').on('input', function() {
filterTags(this.value);
});
// Filter buttons
$('.filter-btn').click(function() {
$('.filter-btn').removeClass('active');
$(this).addClass('active');
const filter = $(this).data('filter');
applyFilter(filter);
});
});
function markModified(element) {
const id = element.id;
const currentValue = $(element).val();
const originalValue = originalValues[id];
if (currentValue !== originalValue) {
modifiedTags.add(id);
$(element).closest('.tag-row').addClass('modified');
// Add modified indicator
if (!$(element).closest('.tag-row').find('.modified-indicator').length) {
$(element).closest('.tag-name').append('<span class="modified-indicator"></span>');
}
} else {
modifiedTags.delete(id);
$(element).closest('.tag-row').removeClass('modified');
$(element).closest('.tag-row').find('.modified-indicator').remove();
}
updateChangesDisplay();
updateModifiedCount();
}
function resetTag(tagId) {
const element = document.getElementById(tagId);
const originalValue = originalValues[tagId];
$(element).val(originalValue);
modifiedTags.delete(tagId);
$(element).closest('.tag-row').removeClass('modified');
$(element).closest('.tag-row').find('.modified-indicator').remove();
updateChangesDisplay();
updateModifiedCount();
}
function resetChanges() {
if (modifiedTags.size === 0) {
showAlert('No changes to reset', 'info');
return;
}
if (confirm('Are you sure you want to reset all changes? This action cannot be undone.')) {
modifiedTags.forEach(tagId => {
resetTag(tagId);
});
showAlert('All changes have been reset', 'success');
}
}
function updateModifiedCount() {
$('#modified-tags').text(`${modifiedTags.size} modified`);
}
function updateChangesDisplay() {
const changesList = document.getElementById('changes-list');
const changesCount = document.getElementById('changes-count');
const changesSection = document.getElementById('changes-summary');
if (modifiedTags.size === 0) {
changesSection.classList.remove('show');
return;
}
changesSection.classList.add('show');
changesCount.textContent = modifiedTags.size;
let html = '';
modifiedTags.forEach(tagId => {
const element = document.getElementById(tagId);
const tagName = $(element).closest('.tag-row').find('.tag-name').text().trim();
const oldValue = originalValues[tagId] || 'Empty';
const newValue = $(element).val() || 'Empty';
html += `
<div class="change-item">
<div class="change-tag">${tagName}</div>
<div class="change-values">
<span class="old-value">${oldValue}</span>
<i class="fas fa-arrow-right mx-2"></i>
<span class="new-value">${newValue}</span>
</div>
</div>
`;
});
changesList.innerHTML = html;
}
function scheduleAutoSave() {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(autoSave, 30000); // Auto-save after 30 seconds
}
function autoSave() {
if (modifiedTags.size === 0) return;
const changes = {};
modifiedTags.forEach(tagId => {
const element = document.getElementById(tagId);
changes[tagId] = $(element).val();
});
fetch(`/radiology/dicom/{{ file.id }}/metadata/auto-save/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json'
},
body: JSON.stringify(changes)
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAutoSaveIndicator();
}
})
.catch(error => {
console.error('Auto-save failed:', error);
});
}
function showAutoSaveIndicator() {
const indicator = document.getElementById('auto-save-indicator');
indicator.style.display = 'block';
setTimeout(() => {
indicator.style.display = 'none';
}, 2000);
}
function saveChanges() {
if (modifiedTags.size === 0) {
showAlert('No changes to save', 'info');
return;
}
const changes = {};
modifiedTags.forEach(tagId => {
const element = document.getElementById(tagId);
changes[tagId] = $(element).val();
});
fetch(`/radiology/dicom/{{ file.id }}/metadata/save/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json'
},
body: JSON.stringify(changes)
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('Metadata saved successfully', 'success');
// Update original values
modifiedTags.forEach(tagId => {
const element = document.getElementById(tagId);
originalValues[tagId] = $(element).val();
});
// Clear modifications
modifiedTags.clear();
$('.tag-row').removeClass('modified');
$('.modified-indicator').remove();
updateChangesDisplay();
updateModifiedCount();
} else {
showAlert('Error saving metadata: ' + (data.error || 'Unknown error'), 'danger');
}
})
.catch(error => {
showAlert('Error saving metadata', 'danger');
});
}
function validateMetadata() {
const formData = {};
$('input, select, textarea').each(function() {
formData[this.id] = $(this).val();
});
fetch(`/radiology/dicom/{{ file.id }}/metadata/validate/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
displayValidationResults(data);
})
.catch(error => {
showAlert('Error validating metadata', 'danger');
});
}
function displayValidationResults(data) {
const errorsSection = document.getElementById('validation-errors');
const errorsList = document.getElementById('error-list');
if (data.errors && data.errors.length > 0) {
errorsSection.classList.add('show');
let html = '';
data.errors.forEach(error => {
html += `
<div class="error-item">
<i class="fas fa-exclamation-circle me-2"></i>
${error.message}
</div>
`;
});
errorsList.innerHTML = html;
} else {
errorsSection.classList.remove('show');
showAlert('Metadata validation passed!', 'success');
}
}
function exportMetadata() {
window.open(`/radiology/dicom/{{ file.id }}/metadata/export/`, '_blank');
}
function generateUID(tagId) {
fetch('/radiology/dicom/generate-uid/', {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
const element = document.getElementById(tagId);
$(element).val(data.uid);
markModified(element);
}
})
.catch(error => {
showAlert('Error generating UID', 'danger');
});
}
function filterTags(searchTerm) {
const rows = document.querySelectorAll('.tag-row');
rows.forEach(row => {
const tagId = row.querySelector('.tag-id').textContent;
const tagName = row.querySelector('.tag-name').textContent;
const tagValue = row.querySelector('.tag-value input, .tag-value select, .tag-value textarea').value;
const matches = tagId.toLowerCase().includes(searchTerm.toLowerCase()) ||
tagName.toLowerCase().includes(searchTerm.toLowerCase()) ||
tagValue.toLowerCase().includes(searchTerm.toLowerCase());
row.style.display = matches ? 'grid' : 'none';
});
}
function applyFilter(filter) {
const rows = document.querySelectorAll('.tag-row');
rows.forEach(row => {
let show = true;
switch (filter) {
case 'modified':
show = row.classList.contains('modified');
break;
case 'required':
show = row.querySelector('.required-field') !== null;
break;
case 'patient':
show = row.closest('#patient-tab') !== null;
break;
case 'study':
show = row.closest('#study-tab') !== null;
break;
case 'series':
show = row.closest('#series-tab') !== null;
break;
case 'image':
show = row.closest('#image-tab') !== null;
break;
case 'technical':
show = row.closest('#technical-tab') !== null;
break;
default:
show = true;
}
row.style.display = show ? 'grid' : 'none';
});
}
function openViewer() {
window.open(`/radiology/dicom/{{ file.id }}/viewer/`, '_blank');
}
function downloadFile() {
window.open(`/radiology/dicom/{{ file.id }}/download/`, '_blank');
}
function validateDICOM() {
fetch(`/radiology/dicom/{{ file.id }}/validate/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
})
.then(response => response.json())
.then(data => {
if (data.valid) {
showAlert('DICOM file is valid', 'success');
} else {
showAlert('DICOM validation failed: ' + data.errors.join(', '), 'danger');
}
})
.catch(error => {
showAlert('Error validating DICOM file', 'danger');
});
}
function anonymizeFile() {
if (confirm('Are you sure you want to anonymize this DICOM file? This will remove patient identifying information.')) {
window.location.href = `/radiology/dicom/{{ file.id }}/anonymize/`;
}
}
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1060; min-width: 300px;';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
</script>
{% endblock %}