2025-08-12 13:33:25 +03:00

876 lines
31 KiB
HTML

{% extends "base.html" %}
{% load static %}
{% block title %}DICOM Upload{% endblock %}
{% block css %}
<style>
.upload-zone {
border: 2px dashed #dee2e6;
border-radius: 8px;
padding: 40px;
text-align: center;
background: #f8f9fa;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-zone.dragover {
border-color: #007bff;
background: #e3f2fd;
}
.upload-zone:hover {
border-color: #007bff;
background: #f0f8ff;
}
.file-list {
max-height: 400px;
overflow-y: auto;
}
.file-item {
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
background: white;
}
.file-item.processing {
background: #fff3cd;
border-color: #ffeaa7;
}
.file-item.success {
background: #d4edda;
border-color: #c3e6cb;
}
.file-item.error {
background: #f8d7da;
border-color: #f5c6cb;
}
.progress-container {
margin-top: 10px;
}
.dicom-info {
font-size: 0.875rem;
color: #6c757d;
}
.upload-stats {
background: #f8f9fa;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex align-items-center mb-3">
<div>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'radiology:dashboard' %}">Radiology</a></li>
<li class="breadcrumb-item active">DICOM Upload</li>
</ol>
<h1 class="page-header mb-0">DICOM Upload</h1>
</div>
<div class="ms-auto">
<button type="button" class="btn btn-outline-secondary me-2" data-bs-toggle="modal" data-bs-target="#settingsModal">
<i class="fas fa-cog me-1"></i>Settings
</button>
<button type="button" class="btn btn-outline-info" data-bs-toggle="modal" data-bs-target="#helpModal">
<i class="fas fa-question-circle me-1"></i>Help
</button>
</div>
</div>
<!-- Upload Statistics -->
<div class="upload-stats">
<div class="row text-center">
<div class="col-md-3">
<div class="h4 text-primary mb-1" id="totalFiles">0</div>
<div class="text-muted">Total Files</div>
</div>
<div class="col-md-3">
<div class="h4 text-success mb-1" id="successfulUploads">0</div>
<div class="text-muted">Successful</div>
</div>
<div class="col-md-3">
<div class="h4 text-warning mb-1" id="processingFiles">0</div>
<div class="text-muted">Processing</div>
</div>
<div class="col-md-3">
<div class="h4 text-danger mb-1" id="failedUploads">0</div>
<div class="text-muted">Failed</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xl-8">
<!-- Upload Zone -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">
<i class="fas fa-cloud-upload-alt me-2"></i>
Upload DICOM Files
</h4>
<div class="card-toolbar">
<button type="button" class="btn btn-sm btn-outline-primary" onclick="selectFiles()">
<i class="fas fa-folder-open me-1"></i>Browse Files
</button>
</div>
</div>
<div class="card-body">
<div class="upload-zone" id="uploadZone" onclick="selectFiles()">
<i class="fas fa-cloud-upload-alt fa-3x text-muted mb-3"></i>
<h5 class="text-muted">Drag and drop DICOM files here</h5>
<p class="text-muted mb-3">or click to browse and select files</p>
<div class="text-muted small">
Supported formats: .dcm, .dicom, .ima<br>
Maximum file size: 100MB per file
</div>
</div>
<input type="file" id="fileInput" multiple accept=".dcm,.dicom,.ima" style="display: none;">
<!-- Upload Controls -->
<div class="d-flex justify-content-between align-items-center mt-3">
<div>
<button type="button" class="btn btn-primary" id="uploadBtn" onclick="startUpload()" disabled>
<i class="fas fa-upload me-2"></i>Start Upload
</button>
<button type="button" class="btn btn-outline-secondary" id="pauseBtn" onclick="pauseUpload()" disabled>
<i class="fas fa-pause me-2"></i>Pause
</button>
<button type="button" class="btn btn-outline-danger" id="cancelBtn" onclick="cancelUpload()" disabled>
<i class="fas fa-times me-2"></i>Cancel
</button>
</div>
<div>
<button type="button" class="btn btn-outline-warning" onclick="clearCompleted()">
<i class="fas fa-broom me-2"></i>Clear Completed
</button>
<button type="button" class="btn btn-outline-danger" onclick="clearAll()">
<i class="fas fa-trash me-2"></i>Clear All
</button>
</div>
</div>
</div>
</div>
<!-- File List -->
<div class="card">
<div class="card-header">
<h4 class="card-title">
<i class="fas fa-list me-2"></i>
Upload Queue
</h4>
<div class="card-toolbar">
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary" onclick="filterFiles('all')">All</button>
<button type="button" class="btn btn-outline-warning" onclick="filterFiles('processing')">Processing</button>
<button type="button" class="btn btn-outline-success" onclick="filterFiles('success')">Success</button>
<button type="button" class="btn btn-outline-danger" onclick="filterFiles('error')">Failed</button>
</div>
</div>
</div>
<div class="card-body">
<div class="file-list" id="fileList">
<div class="text-center text-muted py-5" id="emptyState">
<i class="fas fa-file-medical fa-3x mb-3"></i>
<h5>No files selected</h5>
<p>Select DICOM files to begin uploading</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4">
<!-- Upload Progress -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Upload Progress</h4>
</div>
<div class="card-body">
<div class="mb-3">
<div class="d-flex justify-content-between mb-1">
<span>Overall Progress</span>
<span id="overallProgress">0%</span>
</div>
<div class="progress">
<div class="progress-bar" id="overallProgressBar" style="width: 0%"></div>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between mb-1">
<span>Current File</span>
<span id="currentFileProgress">0%</span>
</div>
<div class="progress">
<div class="progress-bar bg-info" id="currentFileProgressBar" style="width: 0%"></div>
</div>
</div>
<div class="row text-center">
<div class="col-6">
<div class="text-muted small">Upload Speed</div>
<div class="fw-bold" id="uploadSpeed">0 MB/s</div>
</div>
<div class="col-6">
<div class="text-muted small">Time Remaining</div>
<div class="fw-bold" id="timeRemaining">--:--</div>
</div>
</div>
</div>
</div>
<!-- DICOM Validation -->
<div class="card mb-4">
<div class="card-header">
<h4 class="card-title">Validation Settings</h4>
</div>
<div class="card-body">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="validateDicom" checked>
<label class="form-check-label" for="validateDicom">
Validate DICOM format
</label>
</div>
<div class="form-text">Check file format and structure</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="checkDuplicates" checked>
<label class="form-check-label" for="checkDuplicates">
Check for duplicates
</label>
</div>
<div class="form-text">Prevent duplicate uploads</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="autoAssignStudy">
<label class="form-check-label" for="autoAssignStudy">
Auto-assign to study
</label>
</div>
<div class="form-text">Automatically match to existing studies</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="generateThumbnails" checked>
<label class="form-check-label" for="generateThumbnails">
Generate thumbnails
</label>
</div>
<div class="form-text">Create preview images</div>
</div>
</div>
</div>
<!-- Recent Uploads -->
<div class="card">
<div class="card-header">
<h4 class="card-title">Recent Uploads</h4>
</div>
<div class="card-body">
<div id="recentUploads">
{% for upload in recent_uploads %}
<div class="d-flex align-items-center mb-2">
<div class="flex-shrink-0">
<i class="fas fa-file-medical text-primary"></i>
</div>
<div class="flex-grow-1 ms-2">
<div class="small fw-bold">{{ upload.filename|truncatechars:20 }}</div>
<div class="small text-muted">{{ upload.uploaded_at|timesince }} ago</div>
</div>
<div class="flex-shrink-0">
<span class="badge bg-success">Success</span>
</div>
</div>
{% empty %}
<div class="text-center text-muted">
<i class="fas fa-history fa-2x mb-2"></i>
<div>No recent uploads</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<!-- Settings Modal -->
<div class="modal fade" id="settingsModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Upload Settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Concurrent Uploads</label>
<select class="form-select" id="concurrentUploads">
<option value="1">1 (Sequential)</option>
<option value="2" selected>2</option>
<option value="3">3</option>
<option value="5">5</option>
</select>
<div class="form-text">Number of files to upload simultaneously</div>
</div>
<div class="mb-3">
<label class="form-label">Chunk Size (MB)</label>
<select class="form-select" id="chunkSize">
<option value="1">1 MB</option>
<option value="5" selected>5 MB</option>
<option value="10">10 MB</option>
<option value="20">20 MB</option>
</select>
<div class="form-text">Size of upload chunks for large files</div>
</div>
<div class="mb-3">
<label class="form-label">Retry Attempts</label>
<select class="form-select" id="retryAttempts">
<option value="0">No retries</option>
<option value="1">1 retry</option>
<option value="3" selected>3 retries</option>
<option value="5">5 retries</option>
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="pauseOnError">
<label class="form-check-label" for="pauseOnError">
Pause upload on error
</label>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="showNotifications" checked>
<label class="form-check-label" for="showNotifications">
Show desktop notifications
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveSettings()">Save Settings</button>
</div>
</div>
</div>
</div>
<!-- Help Modal -->
<div class="modal fade" id="helpModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">DICOM Upload Help</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h6>Supported Formats</h6>
<ul>
<li>.dcm - Standard DICOM files</li>
<li>.dicom - DICOM files</li>
<li>.ima - Image files</li>
</ul>
<h6>File Requirements</h6>
<ul>
<li>Maximum file size: 100MB</li>
<li>Valid DICOM header required</li>
<li>Patient information must be present</li>
</ul>
</div>
<div class="col-md-6">
<h6>Upload Process</h6>
<ol>
<li>Select or drag DICOM files</li>
<li>Files are validated automatically</li>
<li>Click "Start Upload" to begin</li>
<li>Monitor progress in real-time</li>
<li>Review results and handle errors</li>
</ol>
<h6>Troubleshooting</h6>
<ul>
<li>Check file format if validation fails</li>
<li>Ensure stable internet connection</li>
<li>Contact IT support for persistent issues</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
let uploadQueue = [];
let isUploading = false;
let isPaused = false;
let currentUploadIndex = 0;
let uploadStats = {
total: 0,
successful: 0,
processing: 0,
failed: 0
};
document.addEventListener('DOMContentLoaded', function() {
setupEventListeners();
loadSettings();
});
function setupEventListeners() {
const uploadZone = document.getElementById('uploadZone');
const fileInput = document.getElementById('fileInput');
// Drag and drop events
uploadZone.addEventListener('dragover', handleDragOver);
uploadZone.addEventListener('dragleave', handleDragLeave);
uploadZone.addEventListener('drop', handleDrop);
// File input change
fileInput.addEventListener('change', handleFileSelect);
}
function handleDragOver(e) {
e.preventDefault();
e.currentTarget.classList.add('dragover');
}
function handleDragLeave(e) {
e.preventDefault();
e.currentTarget.classList.remove('dragover');
}
function handleDrop(e) {
e.preventDefault();
e.currentTarget.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files);
addFilesToQueue(files);
}
function selectFiles() {
document.getElementById('fileInput').click();
}
function handleFileSelect(e) {
const files = Array.from(e.target.files);
addFilesToQueue(files);
}
function addFilesToQueue(files) {
const validFiles = files.filter(file => {
const validExtensions = ['.dcm', '.dicom', '.ima'];
const extension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
return validExtensions.includes(extension) && file.size <= 100 * 1024 * 1024; // 100MB limit
});
validFiles.forEach(file => {
const fileItem = {
id: Date.now() + Math.random(),
file: file,
status: 'pending',
progress: 0,
error: null,
dicomInfo: null
};
uploadQueue.push(fileItem);
addFileToList(fileItem);
});
updateStats();
updateUI();
// Hide empty state
document.getElementById('emptyState').style.display = 'none';
}
function addFileToList(fileItem) {
const fileList = document.getElementById('fileList');
const fileDiv = document.createElement('div');
fileDiv.className = 'file-item';
fileDiv.id = `file-${fileItem.id}`;
fileDiv.innerHTML = `
<div class="d-flex align-items-center justify-content-between">
<div class="flex-grow-1">
<div class="fw-bold">${fileItem.file.name}</div>
<div class="small text-muted">
${formatFileSize(fileItem.file.size)}
<span class="status-text">Pending</span>
</div>
<div class="dicom-info" id="dicom-info-${fileItem.id}"></div>
</div>
<div class="flex-shrink-0">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeFile('${fileItem.id}')">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="progress-container" style="display: none;">
<div class="progress">
<div class="progress-bar" id="progress-${fileItem.id}" style="width: 0%"></div>
</div>
</div>
`;
fileList.appendChild(fileDiv);
// Validate DICOM file
if (document.getElementById('validateDicom').checked) {
validateDicomFile(fileItem);
}
}
function validateDicomFile(fileItem) {
const reader = new FileReader();
reader.onload = function(e) {
try {
// Basic DICOM validation (check for DICM magic number)
const arrayBuffer = e.target.result;
const view = new DataView(arrayBuffer);
// Check for DICM at offset 128
if (arrayBuffer.byteLength > 132) {
const dicm = String.fromCharCode(
view.getUint8(128),
view.getUint8(129),
view.getUint8(130),
view.getUint8(131)
);
if (dicm === 'DICM') {
fileItem.status = 'validated';
updateFileStatus(fileItem.id, 'Validated', 'success');
// Extract basic DICOM info (simplified)
extractDicomInfo(fileItem, arrayBuffer);
} else {
fileItem.status = 'invalid';
fileItem.error = 'Invalid DICOM format';
updateFileStatus(fileItem.id, 'Invalid DICOM', 'error');
}
} else {
fileItem.status = 'invalid';
fileItem.error = 'File too small to be valid DICOM';
updateFileStatus(fileItem.id, 'Invalid file', 'error');
}
} catch (error) {
fileItem.status = 'invalid';
fileItem.error = 'Validation error';
updateFileStatus(fileItem.id, 'Validation failed', 'error');
}
};
reader.readAsArrayBuffer(fileItem.file.slice(0, 1024)); // Read first 1KB for validation
}
function extractDicomInfo(fileItem, arrayBuffer) {
// Simplified DICOM info extraction
// In a real implementation, you would use a proper DICOM parser
const infoDiv = document.getElementById(`dicom-info-${fileItem.id}`);
infoDiv.innerHTML = `
<small class="text-success">
<i class="fas fa-check-circle me-1"></i>
Valid DICOM file • Modality: Unknown • Patient: Unknown
</small>
`;
}
function updateFileStatus(fileId, statusText, statusClass) {
const fileDiv = document.getElementById(`file-${fileId}`);
if (fileDiv) {
fileDiv.className = `file-item ${statusClass}`;
fileDiv.querySelector('.status-text').textContent = statusText;
}
}
function removeFile(fileId) {
uploadQueue = uploadQueue.filter(item => item.id !== fileId);
document.getElementById(`file-${fileId}`).remove();
updateStats();
updateUI();
if (uploadQueue.length === 0) {
document.getElementById('emptyState').style.display = 'block';
}
}
function startUpload() {
if (uploadQueue.length === 0) return;
isUploading = true;
isPaused = false;
currentUploadIndex = 0;
updateUI();
processUploadQueue();
}
function pauseUpload() {
isPaused = true;
updateUI();
}
function cancelUpload() {
isUploading = false;
isPaused = false;
// Reset all pending/processing files
uploadQueue.forEach(item => {
if (item.status === 'processing' || item.status === 'pending') {
item.status = 'cancelled';
updateFileStatus(item.id, 'Cancelled', 'error');
}
});
updateStats();
updateUI();
}
function processUploadQueue() {
if (!isUploading || isPaused) return;
const concurrentUploads = parseInt(document.getElementById('concurrentUploads').value);
const processingFiles = uploadQueue.filter(item => item.status === 'processing');
if (processingFiles.length < concurrentUploads) {
const nextFile = uploadQueue.find(item => item.status === 'validated' || item.status === 'pending');
if (nextFile) {
uploadFile(nextFile);
}
}
// Check if all uploads are complete
const remainingFiles = uploadQueue.filter(item =>
item.status === 'pending' || item.status === 'processing' || item.status === 'validated'
);
if (remainingFiles.length === 0) {
isUploading = false;
updateUI();
showNotification('Upload complete!', 'All files have been processed.');
} else {
setTimeout(processUploadQueue, 1000);
}
}
function uploadFile(fileItem) {
fileItem.status = 'processing';
updateFileStatus(fileItem.id, 'Uploading...', 'processing');
const progressDiv = document.querySelector(`#file-${fileItem.id} .progress-container`);
progressDiv.style.display = 'block';
// Simulate file upload with progress
simulateUpload(fileItem);
}
function simulateUpload(fileItem) {
let progress = 0;
const interval = setInterval(() => {
if (isPaused) return;
progress += Math.random() * 10;
if (progress > 100) progress = 100;
fileItem.progress = progress;
updateFileProgress(fileItem.id, progress);
if (progress >= 100) {
clearInterval(interval);
// Simulate success/failure
if (Math.random() > 0.1) { // 90% success rate
fileItem.status = 'success';
updateFileStatus(fileItem.id, 'Upload complete', 'success');
uploadStats.successful++;
} else {
fileItem.status = 'error';
fileItem.error = 'Upload failed';
updateFileStatus(fileItem.id, 'Upload failed', 'error');
uploadStats.failed++;
}
uploadStats.processing--;
updateStats();
updateOverallProgress();
}
}, 200);
}
function updateFileProgress(fileId, progress) {
const progressBar = document.getElementById(`progress-${fileId}`);
if (progressBar) {
progressBar.style.width = `${progress}%`;
}
}
function updateStats() {
uploadStats.total = uploadQueue.length;
uploadStats.processing = uploadQueue.filter(item => item.status === 'processing').length;
uploadStats.successful = uploadQueue.filter(item => item.status === 'success').length;
uploadStats.failed = uploadQueue.filter(item => item.status === 'error' || item.status === 'invalid').length;
document.getElementById('totalFiles').textContent = uploadStats.total;
document.getElementById('successfulUploads').textContent = uploadStats.successful;
document.getElementById('processingFiles').textContent = uploadStats.processing;
document.getElementById('failedUploads').textContent = uploadStats.failed;
}
function updateOverallProgress() {
const completedFiles = uploadStats.successful + uploadStats.failed;
const progress = uploadStats.total > 0 ? (completedFiles / uploadStats.total) * 100 : 0;
document.getElementById('overallProgress').textContent = `${Math.round(progress)}%`;
document.getElementById('overallProgressBar').style.width = `${progress}%`;
}
function updateUI() {
const hasFiles = uploadQueue.length > 0;
const canUpload = hasFiles && !isUploading && uploadQueue.some(item =>
item.status === 'validated' || item.status === 'pending'
);
document.getElementById('uploadBtn').disabled = !canUpload;
document.getElementById('pauseBtn').disabled = !isUploading || isPaused;
document.getElementById('cancelBtn').disabled = !isUploading;
}
function clearCompleted() {
const completedFiles = uploadQueue.filter(item =>
item.status === 'success' || item.status === 'error' || item.status === 'invalid'
);
completedFiles.forEach(item => {
document.getElementById(`file-${item.id}`).remove();
});
uploadQueue = uploadQueue.filter(item =>
item.status !== 'success' && item.status !== 'error' && item.status !== 'invalid'
);
updateStats();
updateUI();
if (uploadQueue.length === 0) {
document.getElementById('emptyState').style.display = 'block';
}
}
function clearAll() {
if (confirm('Clear all files from the upload queue?')) {
uploadQueue = [];
document.getElementById('fileList').innerHTML = `
<div class="text-center text-muted py-5" id="emptyState">
<i class="fas fa-file-medical fa-3x mb-3"></i>
<h5>No files selected</h5>
<p>Select DICOM files to begin uploading</p>
</div>
`;
updateStats();
updateUI();
}
}
function filterFiles(status) {
const fileItems = document.querySelectorAll('.file-item');
fileItems.forEach(item => {
if (status === 'all') {
item.style.display = 'block';
} else {
item.style.display = item.classList.contains(status) ? 'block' : 'none';
}
});
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function saveSettings() {
const settings = {
concurrentUploads: document.getElementById('concurrentUploads').value,
chunkSize: document.getElementById('chunkSize').value,
retryAttempts: document.getElementById('retryAttempts').value,
pauseOnError: document.getElementById('pauseOnError').checked,
showNotifications: document.getElementById('showNotifications').checked
};
localStorage.setItem('dicomUploadSettings', JSON.stringify(settings));
const modal = bootstrap.Modal.getInstance(document.getElementById('settingsModal'));
modal.hide();
}
function loadSettings() {
const settings = localStorage.getItem('dicomUploadSettings');
if (settings) {
const parsed = JSON.parse(settings);
document.getElementById('concurrentUploads').value = parsed.concurrentUploads || '2';
document.getElementById('chunkSize').value = parsed.chunkSize || '5';
document.getElementById('retryAttempts').value = parsed.retryAttempts || '3';
document.getElementById('pauseOnError').checked = parsed.pauseOnError || false;
document.getElementById('showNotifications').checked = parsed.showNotifications !== false;
}
}
function showNotification(title, message) {
if (document.getElementById('showNotifications').checked && 'Notification' in window) {
if (Notification.permission === 'granted') {
new Notification(title, { body: message });
} else if (Notification.permission !== 'denied') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
new Notification(title, { body: message });
}
});
}
}
}
</script>
{% endblock %}