1145 lines
40 KiB
HTML
1145 lines
40 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}DICOM Files Management{% endblock %}
|
|
|
|
{% block css %}
|
|
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
|
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
|
<link href="{% static 'plugins/datatables.net-buttons-bs5/css/buttons.bootstrap5.min.css' %}" rel="stylesheet" />
|
|
<style>
|
|
.page-header-section {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border-radius: 0.5rem;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.stats-cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
padding: 1.5rem;
|
|
text-align: center;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.stat-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.stat-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: var(--card-color);
|
|
}
|
|
|
|
.stat-icon {
|
|
width: 50px;
|
|
height: 50px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0 auto 1rem;
|
|
color: white;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: #495057;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.stat-label {
|
|
color: #6c757d;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.filters-section {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
padding: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.filter-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
align-items: end;
|
|
}
|
|
|
|
.files-table-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;
|
|
}
|
|
|
|
.file-thumbnail {
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 0.375rem;
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #6c757d;
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.file-thumbnail:hover {
|
|
background: #e3f2fd;
|
|
color: #1976d2;
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.file-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.file-name {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.file-details {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.file-meta {
|
|
font-size: 0.75rem;
|
|
color: #adb5bd;
|
|
}
|
|
|
|
.patient-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.patient-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: #007bff;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.modality-badge {
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.modality-ct { background: #e3f2fd; color: #1976d2; }
|
|
.modality-mri { background: #f3e5f5; color: #7b1fa2; }
|
|
.modality-xr { background: #e8f5e8; color: #388e3c; }
|
|
.modality-us { background: #fff3e0; color: #f57c00; }
|
|
.modality-cr { background: #fce4ec; color: #c2185b; }
|
|
.modality-dx { background: #e0f2f1; color: #00796b; }
|
|
|
|
.status-badge {
|
|
padding: 0.375rem 0.75rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.status-uploaded { background: #e8f5e8; color: #2e7d32; }
|
|
.status-processing { background: #fff3cd; color: #856404; }
|
|
.status-processed { background: #d1ecf1; color: #0c5460; }
|
|
.status-archived { background: #f8f9fa; color: #6c757d; }
|
|
.status-error { background: #f8d7da; color: #721c24; }
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.btn-action {
|
|
padding: 0.375rem 0.5rem;
|
|
border: none;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.btn-view { background: #e3f2fd; color: #1976d2; }
|
|
.btn-download { background: #e8f5e8; color: #2e7d32; }
|
|
.btn-edit { background: #fff3e0; color: #f57c00; }
|
|
.btn-delete { background: #ffebee; color: #d32f2f; }
|
|
.btn-analyze { background: #f3e5f5; color: #7b1fa2; }
|
|
|
|
.btn-action:hover {
|
|
transform: scale(1.05);
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.bulk-actions {
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
display: none;
|
|
}
|
|
|
|
.bulk-actions.show {
|
|
display: block;
|
|
}
|
|
|
|
.quick-filters {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.quick-filter {
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid #dee2e6;
|
|
background: white;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 0.875rem;
|
|
text-decoration: none;
|
|
color: #495057;
|
|
}
|
|
|
|
.quick-filter:hover, .quick-filter.active {
|
|
background: #007bff;
|
|
color: white;
|
|
border-color: #007bff;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.file-size {
|
|
font-weight: bold;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.upload-progress {
|
|
background: #e9ecef;
|
|
border-radius: 0.25rem;
|
|
height: 4px;
|
|
overflow: hidden;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.progress-bar {
|
|
background: #007bff;
|
|
height: 100%;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.dicom-tags {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.study-info {
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
padding: 0.5rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.study-date {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.study-description {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.page-header-section {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.stats-cards {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.filter-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.quick-filters {
|
|
justify-content: center;
|
|
}
|
|
|
|
.action-buttons {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.file-thumbnail {
|
|
width: 40px;
|
|
height: 40px;
|
|
font-size: 1rem;
|
|
}
|
|
}
|
|
|
|
@media print {
|
|
.filters-section, .bulk-actions, .action-buttons {
|
|
display: none !important;
|
|
}
|
|
|
|
.section-header {
|
|
background: none;
|
|
border-bottom: 2px solid #000;
|
|
color: #000;
|
|
}
|
|
}
|
|
</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 'radiology:dashboard' %}">Radiology</a></li>
|
|
<li class="breadcrumb-item active">DICOM Files</li>
|
|
</ol>
|
|
<h1 class="page-header mb-0">
|
|
<i class="fas fa-file-medical me-2"></i>DICOM Files Management
|
|
</h1>
|
|
</div>
|
|
<div class="ms-auto">
|
|
<button type="button" class="btn btn-outline-secondary me-2" onclick="exportFiles()">
|
|
<i class="fas fa-download me-1"></i>Export
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info me-2" onclick="bulkUpload()">
|
|
<i class="fas fa-upload me-1"></i>Bulk Upload
|
|
</button>
|
|
{# <a href="{% url 'radiology:dicom_upload' %}" class="btn btn-primary">#}
|
|
{# <i class="fas fa-plus me-1"></i>Upload DICOM#}
|
|
{# </a>#}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="stats-cards">
|
|
<div class="stat-card" style="--card-color: #007bff;">
|
|
<div class="stat-icon" style="background: #007bff;">
|
|
<i class="fas fa-file-medical"></i>
|
|
</div>
|
|
<div class="stat-number">{{ stats.total_files|default:0 }}</div>
|
|
<div class="stat-label">Total Files</div>
|
|
</div>
|
|
|
|
<div class="stat-card" style="--card-color: #28a745;">
|
|
<div class="stat-icon" style="background: #28a745;">
|
|
<i class="fas fa-check-circle"></i>
|
|
</div>
|
|
<div class="stat-number">{{ stats.processed_files|default:0 }}</div>
|
|
<div class="stat-label">Processed</div>
|
|
</div>
|
|
|
|
<div class="stat-card" style="--card-color: #ffc107;">
|
|
<div class="stat-icon" style="background: #ffc107;">
|
|
<i class="fas fa-clock"></i>
|
|
</div>
|
|
<div class="stat-number">{{ stats.pending_files|default:0 }}</div>
|
|
<div class="stat-label">Processing</div>
|
|
</div>
|
|
|
|
<div class="stat-card" style="--card-color: #17a2b8;">
|
|
<div class="stat-icon" style="background: #17a2b8;">
|
|
<i class="fas fa-hdd"></i>
|
|
</div>
|
|
<div class="stat-number">{{ stats.total_size|default:"0 GB" }}</div>
|
|
<div class="stat-label">Storage Used</div>
|
|
</div>
|
|
|
|
<div class="stat-card" style="--card-color: #6f42c1;">
|
|
<div class="stat-icon" style="background: #6f42c1;">
|
|
<i class="fas fa-users"></i>
|
|
</div>
|
|
<div class="stat-number">{{ stats.unique_patients|default:0 }}</div>
|
|
<div class="stat-label">Patients</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Filters -->
|
|
<div class="quick-filters">
|
|
<a href="?status=all" class="quick-filter {% if not request.GET.status or request.GET.status == 'all' %}active{% endif %}">
|
|
<i class="fas fa-list me-1"></i>All Files
|
|
</a>
|
|
<a href="?status=uploaded" class="quick-filter {% if request.GET.status == 'uploaded' %}active{% endif %}">
|
|
<i class="fas fa-upload me-1"></i>Uploaded
|
|
</a>
|
|
<a href="?status=processing" class="quick-filter {% if request.GET.status == 'processing' %}active{% endif %}">
|
|
<i class="fas fa-clock me-1"></i>Processing
|
|
</a>
|
|
<a href="?status=processed" class="quick-filter {% if request.GET.status == 'processed' %}active{% endif %}">
|
|
<i class="fas fa-check me-1"></i>Processed
|
|
</a>
|
|
<a href="?modality=CT" class="quick-filter {% if request.GET.modality == 'CT' %}active{% endif %}">
|
|
<i class="fas fa-x-ray me-1"></i>CT Scans
|
|
</a>
|
|
<a href="?modality=MRI" class="quick-filter {% if request.GET.modality == 'MRI' %}active{% endif %}">
|
|
<i class="fas fa-brain me-1"></i>MRI
|
|
</a>
|
|
<a href="?modality=XR" class="quick-filter {% if request.GET.modality == 'XR' %}active{% endif %}">
|
|
<i class="fas fa-bone me-1"></i>X-Ray
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Filters Section -->
|
|
<div class="filters-section">
|
|
<h6 class="mb-3">
|
|
<i class="fas fa-filter me-2"></i>Advanced Filters
|
|
</h6>
|
|
|
|
<form method="get" id="filter-form">
|
|
<div class="filter-row">
|
|
<div>
|
|
<label class="form-label">Patient Name/ID</label>
|
|
<input type="text" class="form-control" name="patient"
|
|
value="{{ request.GET.patient }}" placeholder="Search patient...">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="form-label">Study Date</label>
|
|
<input type="date" class="form-control" name="study_date"
|
|
value="{{ request.GET.study_date }}">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="form-label">Modality</label>
|
|
<select class="form-select" name="modality">
|
|
<option value="">All Modalities</option>
|
|
<option value="CT" {% if request.GET.modality == 'CT' %}selected{% endif %}>CT</option>
|
|
<option value="MRI" {% if request.GET.modality == 'MRI' %}selected{% endif %}>MRI</option>
|
|
<option value="XR" {% if request.GET.modality == 'XR' %}selected{% endif %}>X-Ray</option>
|
|
<option value="US" {% if request.GET.modality == 'US' %}selected{% endif %}>Ultrasound</option>
|
|
<option value="CR" {% if request.GET.modality == 'CR' %}selected{% endif %}>CR</option>
|
|
<option value="DX" {% if request.GET.modality == 'DX' %}selected{% endif %}>DX</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="form-label">Status</label>
|
|
<select class="form-select" name="status">
|
|
<option value="">All Statuses</option>
|
|
<option value="uploaded" {% if request.GET.status == 'uploaded' %}selected{% endif %}>Uploaded</option>
|
|
<option value="processing" {% if request.GET.status == 'processing' %}selected{% endif %}>Processing</option>
|
|
<option value="processed" {% if request.GET.status == 'processed' %}selected{% endif %}>Processed</option>
|
|
<option value="archived" {% if request.GET.status == 'archived' %}selected{% endif %}>Archived</option>
|
|
<option value="error" {% if request.GET.status == 'error' %}selected{% endif %}>Error</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="form-label">File Size</label>
|
|
<select class="form-select" name="size_range">
|
|
<option value="">Any Size</option>
|
|
<option value="small" {% if request.GET.size_range == 'small' %}selected{% endif %}>< 10 MB</option>
|
|
<option value="medium" {% if request.GET.size_range == 'medium' %}selected{% endif %}>10-100 MB</option>
|
|
<option value="large" {% if request.GET.size_range == 'large' %}selected{% endif %}>100-500 MB</option>
|
|
<option value="xlarge" {% if request.GET.size_range == 'xlarge' %}selected{% endif %}>> 500 MB</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-search me-1"></i>Filter
|
|
</button>
|
|
{# <a href="{% url 'radiology:dicom_file_list' %}" class="btn btn-outline-secondary ms-2">#}
|
|
{# <i class="fas fa-times me-1"></i>Clear#}
|
|
{# </a>#}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Bulk Actions -->
|
|
<div class="bulk-actions" id="bulk-actions">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<span id="selected-count">0</span> files selected
|
|
</div>
|
|
<div>
|
|
<button type="button" class="btn btn-outline-primary btn-sm me-2" onclick="bulkDownload()">
|
|
<i class="fas fa-download me-1"></i>Download Selected
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info btn-sm me-2" onclick="bulkAnalyze()">
|
|
<i class="fas fa-search me-1"></i>Analyze Selected
|
|
</button>
|
|
<button type="button" class="btn btn-outline-warning btn-sm me-2" onclick="bulkArchive()">
|
|
<i class="fas fa-archive me-1"></i>Archive Selected
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger btn-sm" onclick="bulkDelete()">
|
|
<i class="fas fa-trash me-1"></i>Delete Selected
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Files Table -->
|
|
<div class="files-table-section">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-table me-2"></i>DICOM Files ({{ files|length }})
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="auto-refresh">
|
|
<label class="form-check-label" for="auto-refresh">Auto Refresh</label>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="refreshTable()">
|
|
<i class="fas fa-sync"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0" id="files-table">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th width="40">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="select-all">
|
|
</div>
|
|
</th>
|
|
<th>File</th>
|
|
<th>Patient</th>
|
|
<th>Study Info</th>
|
|
<th>Modality</th>
|
|
<th>Size</th>
|
|
<th>Upload Date</th>
|
|
<th>Status</th>
|
|
<th width="150">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for file in files %}
|
|
<tr>
|
|
<td>
|
|
<div class="form-check">
|
|
<input class="form-check-input file-checkbox" type="checkbox" value="{{ file.id }}">
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center gap-3">
|
|
<div class="file-thumbnail" onclick="viewFile({{ file.id }})">
|
|
{% if file.thumbnail %}
|
|
<img src="{{ file.thumbnail.url }}" alt="Thumbnail" style="width: 100%; height: 100%; object-fit: cover; border-radius: 0.375rem;">
|
|
{% else %}
|
|
<i class="fas fa-file-medical"></i>
|
|
{% endif %}
|
|
</div>
|
|
<div class="file-info">
|
|
<div class="file-name">{{ file.filename }}</div>
|
|
<div class="file-details">{{ file.series_description|default:"No description" }}</div>
|
|
<div class="file-meta">
|
|
Instance: {{ file.instance_number|default:"N/A" }} |
|
|
Slice: {{ file.slice_location|default:"N/A" }}
|
|
</div>
|
|
{% if file.dicom_tags %}
|
|
<div class="dicom-tags">
|
|
SOP: {{ file.dicom_tags.SOPInstanceUID|truncatechars:20 }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="patient-info">
|
|
<div class="patient-avatar">
|
|
{{ file.patient_name.0|upper|default:"P" }}
|
|
</div>
|
|
<div>
|
|
<div class="fw-bold">{{ file.patient_name|default:"Unknown Patient" }}</div>
|
|
<small class="text-muted">ID: {{ file.patient_id|default:"N/A" }}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="study-info">
|
|
<div class="study-date">{{ file.study_date|date:"M d, Y"|default:"N/A" }}</div>
|
|
<div class="study-description">{{ file.study_description|truncatechars:30|default:"No description" }}</div>
|
|
{% if file.study_time %}
|
|
<small class="text-muted">{{ file.study_time|time:"g:i A" }}</small>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="modality-badge modality-{{ file.modality|lower }}">
|
|
{{ file.modality|default:"Unknown" }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="file-size">{{ file.file_size|filesizeformat }}</div>
|
|
{% if file.compression_ratio %}
|
|
<small class="text-muted">{{ file.compression_ratio }}% compressed</small>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="fw-bold">{{ file.uploaded_at|date:"M d, Y" }}</div>
|
|
<small class="text-muted">{{ file.uploaded_at|time:"g:i A" }}</small>
|
|
</td>
|
|
<td>
|
|
<span class="status-badge status-{{ file.status }}">
|
|
{{ file.get_status_display }}
|
|
</span>
|
|
{% if file.status == 'processing' %}
|
|
<div class="upload-progress">
|
|
<div class="progress-bar" style="width: {{ file.processing_progress|default:0 }}%"></div>
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button type="button" class="btn-action btn-view"
|
|
onclick="viewFile({{ file.id }})" title="View DICOM">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button type="button" class="btn-action btn-download"
|
|
onclick="downloadFile({{ file.id }})" title="Download">
|
|
<i class="fas fa-download"></i>
|
|
</button>
|
|
<button type="button" class="btn-action btn-analyze"
|
|
onclick="analyzeFile({{ file.id }})" title="Analyze">
|
|
<i class="fas fa-search"></i>
|
|
</button>
|
|
{% if file.can_edit %}
|
|
<button type="button" class="btn-action btn-edit"
|
|
onclick="editFile({{ file.id }})" title="Edit Metadata">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
{% endif %}
|
|
{% if file.can_delete %}
|
|
<button type="button" class="btn-action btn-delete"
|
|
onclick="deleteFile({{ file.id }})" title="Delete">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="9" class="text-center py-4">
|
|
<div class="text-muted">
|
|
<i class="fas fa-file-medical fa-3x mb-3"></i>
|
|
<h5>No DICOM Files Found</h5>
|
|
<p>No DICOM files match your current filters.</p>
|
|
{# <a href="{% url 'radiology:dicom_upload' %}" class="btn btn-primary">#}
|
|
{# <i class="fas fa-plus me-1"></i>Upload First File#}
|
|
{# </a>#}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if is_paginated %}
|
|
<div class="d-flex justify-content-between align-items-center p-3">
|
|
<div class="text-muted">
|
|
Showing {{ files|length }} of {{ total_files }} files
|
|
</div>
|
|
|
|
<nav aria-label="Files pagination">
|
|
<ul class="pagination pagination-sm mb-0">
|
|
{% if page_obj.has_previous %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page=1{{ request.GET.urlencode }}">First</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{{ request.GET.urlencode }}">Previous</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
<li class="page-item active">
|
|
<span class="page-link">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
|
|
</li>
|
|
|
|
{% if page_obj.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{{ request.GET.urlencode }}">Next</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{{ request.GET.urlencode }}">Last</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File Details Modal -->
|
|
<div class="modal fade" id="fileDetailsModal" tabindex="-1">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-file-medical me-2"></i>DICOM File Details
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="file-details-content">
|
|
<!-- File details will be loaded here -->
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>Close
|
|
</button>
|
|
<button type="button" class="btn btn-primary" onclick="openInViewer()">
|
|
<i class="fas fa-eye me-1"></i>Open in Viewer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div class="modal fade" id="deleteModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-exclamation-triangle me-2 text-danger"></i>Delete DICOM File
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-danger">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
Are you sure you want to delete this DICOM file? This action cannot be undone.
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Reason for Deletion</label>
|
|
<textarea class="form-control" id="deletion-reason" rows="3"
|
|
placeholder="Please provide a reason for deleting this file..." required></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>Cancel
|
|
</button>
|
|
<button type="button" class="btn btn-danger" onclick="confirmDeletion()">
|
|
<i class="fas fa-trash me-1"></i>Delete File
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
|
|
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
|
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
|
<script src="{% static 'plugins/datatables.net-buttons-bs5/js/buttons.bootstrap5.min.js' %}"></script>
|
|
|
|
<script>
|
|
let currentFileId = null;
|
|
|
|
$(document).ready(function() {
|
|
// Initialize DataTable
|
|
$('#files-table').DataTable({
|
|
responsive: true,
|
|
pageLength: 25,
|
|
order: [[6, 'desc']], // Sort by upload date
|
|
columnDefs: [
|
|
{ orderable: false, targets: [0, 8] } // Disable sorting for checkbox and actions
|
|
]
|
|
});
|
|
|
|
// Handle select all checkbox
|
|
$('#select-all').change(function() {
|
|
$('.file-checkbox').prop('checked', this.checked);
|
|
updateBulkActions();
|
|
});
|
|
|
|
// Handle individual checkboxes
|
|
$('.file-checkbox').change(function() {
|
|
updateBulkActions();
|
|
|
|
// Update select all checkbox
|
|
const totalCheckboxes = $('.file-checkbox').length;
|
|
const checkedCheckboxes = $('.file-checkbox:checked').length;
|
|
$('#select-all').prop('checked', totalCheckboxes === checkedCheckboxes);
|
|
});
|
|
|
|
// Auto-refresh functionality
|
|
let autoRefreshInterval;
|
|
$('#auto-refresh').change(function() {
|
|
if (this.checked) {
|
|
autoRefreshInterval = setInterval(refreshTable, 30000); // Refresh every 30 seconds
|
|
} else {
|
|
clearInterval(autoRefreshInterval);
|
|
}
|
|
});
|
|
});
|
|
|
|
function updateBulkActions() {
|
|
const selectedCount = $('.file-checkbox:checked').length;
|
|
$('#selected-count').text(selectedCount);
|
|
|
|
if (selectedCount > 0) {
|
|
$('#bulk-actions').addClass('show');
|
|
} else {
|
|
$('#bulk-actions').removeClass('show');
|
|
}
|
|
}
|
|
|
|
function refreshTable() {
|
|
location.reload();
|
|
}
|
|
|
|
function exportFiles() {
|
|
const selectedFiles = $('.file-checkbox:checked').map(function() {
|
|
return this.value;
|
|
}).get();
|
|
|
|
let url = '/radiology/dicom/export/';
|
|
if (selectedFiles.length > 0) {
|
|
url += '?files=' + selectedFiles.join(',');
|
|
}
|
|
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
function bulkUpload() {
|
|
window.location.href = '/radiology/dicom/bulk-upload/';
|
|
}
|
|
|
|
function viewFile(fileId) {
|
|
currentFileId = fileId;
|
|
|
|
fetch(`/radiology/dicom/${fileId}/details/`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
displayFileDetails(data.file);
|
|
new bootstrap.Modal(document.getElementById('fileDetailsModal')).show();
|
|
} else {
|
|
showAlert('Error loading file details', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error loading file details', 'danger');
|
|
});
|
|
}
|
|
|
|
function displayFileDetails(file) {
|
|
const content = `
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6>File Information</h6>
|
|
<table class="table table-sm">
|
|
<tr><td><strong>Filename:</strong></td><td>${file.filename}</td></tr>
|
|
<tr><td><strong>Size:</strong></td><td>${file.file_size}</td></tr>
|
|
<tr><td><strong>Upload Date:</strong></td><td>${file.uploaded_at}</td></tr>
|
|
<tr><td><strong>Status:</strong></td><td>${file.status}</td></tr>
|
|
</table>
|
|
|
|
<h6>Patient Information</h6>
|
|
<table class="table table-sm">
|
|
<tr><td><strong>Name:</strong></td><td>${file.patient_name || 'N/A'}</td></tr>
|
|
<tr><td><strong>ID:</strong></td><td>${file.patient_id || 'N/A'}</td></tr>
|
|
<tr><td><strong>Birth Date:</strong></td><td>${file.patient_birth_date || 'N/A'}</td></tr>
|
|
<tr><td><strong>Sex:</strong></td><td>${file.patient_sex || 'N/A'}</td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<h6>Study Information</h6>
|
|
<table class="table table-sm">
|
|
<tr><td><strong>Study Date:</strong></td><td>${file.study_date || 'N/A'}</td></tr>
|
|
<tr><td><strong>Study Time:</strong></td><td>${file.study_time || 'N/A'}</td></tr>
|
|
<tr><td><strong>Description:</strong></td><td>${file.study_description || 'N/A'}</td></tr>
|
|
<tr><td><strong>Modality:</strong></td><td>${file.modality || 'N/A'}</td></tr>
|
|
</table>
|
|
|
|
<h6>Series Information</h6>
|
|
<table class="table table-sm">
|
|
<tr><td><strong>Series Number:</strong></td><td>${file.series_number || 'N/A'}</td></tr>
|
|
<tr><td><strong>Description:</strong></td><td>${file.series_description || 'N/A'}</td></tr>
|
|
<tr><td><strong>Instance Number:</strong></td><td>${file.instance_number || 'N/A'}</td></tr>
|
|
<tr><td><strong>Slice Location:</strong></td><td>${file.slice_location || 'N/A'}</td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
${file.thumbnail ? `
|
|
<div class="text-center mt-3">
|
|
<h6>Preview</h6>
|
|
<img src="${file.thumbnail}" alt="DICOM Preview" class="img-fluid" style="max-height: 300px;">
|
|
</div>
|
|
` : ''}
|
|
`;
|
|
|
|
document.getElementById('file-details-content').innerHTML = content;
|
|
}
|
|
|
|
function downloadFile(fileId) {
|
|
window.open(`/radiology/dicom/${fileId}/download/`, '_blank');
|
|
}
|
|
|
|
function analyzeFile(fileId) {
|
|
window.location.href = `/radiology/dicom/${fileId}/analyze/`;
|
|
}
|
|
|
|
function editFile(fileId) {
|
|
window.location.href = `/radiology/dicom/${fileId}/edit/`;
|
|
}
|
|
|
|
function deleteFile(fileId) {
|
|
currentFileId = fileId;
|
|
new bootstrap.Modal(document.getElementById('deleteModal')).show();
|
|
}
|
|
|
|
function confirmDeletion() {
|
|
const reason = document.getElementById('deletion-reason').value;
|
|
|
|
if (!reason.trim()) {
|
|
showAlert('Please provide a reason for deletion', 'warning');
|
|
return;
|
|
}
|
|
|
|
fetch(`/radiology/dicom/${currentFileId}/delete/`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
reason: reason
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('DICOM file deleted successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error deleting DICOM file', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error deleting DICOM file', 'danger');
|
|
});
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('deleteModal')).hide();
|
|
}
|
|
|
|
function bulkDownload() {
|
|
const selectedFiles = $('.file-checkbox:checked').map(function() {
|
|
return this.value;
|
|
}).get();
|
|
|
|
if (selectedFiles.length === 0) {
|
|
showAlert('Please select files to download', 'warning');
|
|
return;
|
|
}
|
|
|
|
const url = '/radiology/dicom/bulk-download/?files=' + selectedFiles.join(',');
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
function bulkAnalyze() {
|
|
const selectedFiles = $('.file-checkbox:checked').map(function() {
|
|
return this.value;
|
|
}).get();
|
|
|
|
if (selectedFiles.length === 0) {
|
|
showAlert('Please select files to analyze', 'warning');
|
|
return;
|
|
}
|
|
|
|
if (confirm(`Start analysis for ${selectedFiles.length} selected files?`)) {
|
|
fetch('/radiology/dicom/bulk-analyze/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
file_ids: selectedFiles
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert(`Analysis started for ${data.processed_count} files`, 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error starting analysis', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error starting analysis', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function bulkArchive() {
|
|
const selectedFiles = $('.file-checkbox:checked').map(function() {
|
|
return this.value;
|
|
}).get();
|
|
|
|
if (selectedFiles.length === 0) {
|
|
showAlert('Please select files to archive', 'warning');
|
|
return;
|
|
}
|
|
|
|
if (confirm(`Archive ${selectedFiles.length} selected files?`)) {
|
|
fetch('/radiology/dicom/bulk-archive/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
file_ids: selectedFiles
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert(`${data.archived_count} files archived successfully`, 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error archiving files', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error archiving files', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function bulkDelete() {
|
|
const selectedFiles = $('.file-checkbox:checked').map(function() {
|
|
return this.value;
|
|
}).get();
|
|
|
|
if (selectedFiles.length === 0) {
|
|
showAlert('Please select files to delete', 'warning');
|
|
return;
|
|
}
|
|
|
|
if (confirm(`Are you sure you want to delete ${selectedFiles.length} selected files? This action cannot be undone.`)) {
|
|
fetch('/radiology/dicom/bulk-delete/', {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
file_ids: selectedFiles
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert(`${data.deleted_count} files deleted successfully`, 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error deleting files', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error deleting files', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function openInViewer() {
|
|
if (currentFileId) {
|
|
window.open(`/radiology/dicom/${currentFileId}/viewer/`, '_blank');
|
|
}
|
|
}
|
|
|
|
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 %}
|
|
|