566 lines
25 KiB
HTML
566 lines
25 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}{% if form.instance.pk %}Edit{% else %}Add{% endif %} 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>{% if form.instance.pk %}Edit{% else %}Add{% endif %} Imaging Series</h4>
|
|
<h6>{% if study %}Study: {{ study.study_description|default:"Imaging Study" }}{% endif %}</h6>
|
|
</div>
|
|
<div class="page-btn">
|
|
<a href="{% if study %}{% url 'radiology:imaging_series_list' study.pk %}{% else %}{% url 'radiology:imaging_series_list' form.instance.study.pk %}{% endif %}" class="btn btn-secondary">
|
|
<i class="fas fa-arrow-left me-1"></i>Back to Series List
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="post" enctype="multipart/form-data" id="series-form">
|
|
{% csrf_token %}
|
|
|
|
<!-- Basic Information -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-info-circle me-2"></i>Basic Information
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label class="form-label" for="{{ form.series_number.id_for_label }}">
|
|
Series Number <span class="text-danger">*</span>
|
|
</label>
|
|
{{ form.series_number }}
|
|
{% if form.series_number.errors %}
|
|
<div class="text-danger">
|
|
{% for error in form.series_number.errors %}
|
|
<small>{{ error }}</small>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
<small class="form-text text-muted">Unique series number within the study</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label class="form-label" for="{{ form.modality.id_for_label }}">
|
|
Modality <span class="text-danger">*</span>
|
|
</label>
|
|
{{ form.modality }}
|
|
{% if form.modality.errors %}
|
|
<div class="text-danger">
|
|
{% for error in form.modality.errors %}
|
|
<small>{{ error }}</small>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<div class="form-group">
|
|
<label class="form-label" for="{{ form.series_description.id_for_label }}">
|
|
Series Description
|
|
</label>
|
|
{{ form.series_description }}
|
|
{% if form.series_description.errors %}
|
|
<div class="text-danger">
|
|
{% for error in form.series_description.errors %}
|
|
<small>{{ error }}</small>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
<small class="form-text text-muted">Descriptive name for this imaging series</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label class="form-label" for="{{ form.protocol_name.id_for_label }}">
|
|
Protocol Name
|
|
</label>
|
|
{{ form.protocol_name }}
|
|
{% if form.protocol_name.errors %}
|
|
<div class="text-danger">
|
|
{% for error in form.protocol_name.errors %}
|
|
<small>{{ error }}</small>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label class="form-label" for="{{ form.series_instance_uid.id_for_label }}">
|
|
Series Instance UID <span class="text-danger">*</span>
|
|
</label>
|
|
{{ form.series_instance_uid }}
|
|
{% if form.series_instance_uid.errors %}
|
|
<div class="text-danger">
|
|
{% for error in form.series_instance_uid.errors %}
|
|
<small>{{ error }}</small>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
<small class="form-text text-muted">DICOM Series Instance UID</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Date and Time Information -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-calendar-alt me-2"></i>Date and Time Information
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<div class="form-group">
|
|
<label class="form-label" for="{{ form.series_date.id_for_label }}">
|
|
Series Date <span class="text-danger">*</span>
|
|
</label>
|
|
{{ form.series_date }}
|
|
{% if form.series_date.errors %}
|
|
<div class="text-danger">
|
|
{% for error in form.series_date.errors %}
|
|
<small>{{ error }}</small>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="form-group">
|
|
<label class="form-label" for="{{ form.series_time.id_for_label }}">
|
|
Series Time <span class="text-danger">*</span>
|
|
</label>
|
|
{{ form.series_time }}
|
|
{% if form.series_time.errors %}
|
|
<div class="text-danger">
|
|
{% for error in form.series_time.errors %}
|
|
<small>{{ error }}</small>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="form-group">
|
|
<label class="form-label" for="{{ form.series_datetime.id_for_label }}">
|
|
Series DateTime <span class="text-danger">*</span>
|
|
</label>
|
|
{{ form.series_datetime }}
|
|
{% if form.series_datetime.errors %}
|
|
<div class="text-danger">
|
|
{% for error in form.series_datetime.errors %}
|
|
<small>{{ error }}</small>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
<small class="form-text text-muted">Combined date and time</small>
|
|
</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-6">
|
|
<div class="form-group">
|
|
<label class="form-label" for="{{ form.slice_thickness.id_for_label }}">
|
|
Slice Thickness (mm)
|
|
</label>
|
|
{{ form.slice_thickness }}
|
|
{% if form.slice_thickness.errors %}
|
|
<div class="text-danger">
|
|
{% for error in form.slice_thickness.errors %}
|
|
<small>{{ error }}</small>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label class="form-label" for="{{ form.spacing_between_slices.id_for_label }}">
|
|
Spacing Between Slices (mm)
|
|
</label>
|
|
{{ form.spacing_between_slices }}
|
|
{% if form.spacing_between_slices.errors %}
|
|
<div class="text-danger">
|
|
{% for error in form.spacing_between_slices.errors %}
|
|
<small>{{ error }}</small>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label class="form-label" for="{{ form.pixel_spacing.id_for_label }}">
|
|
Pixel Spacing
|
|
</label>
|
|
{{ form.pixel_spacing }}
|
|
{% if form.pixel_spacing.errors %}
|
|
<div class="text-danger">
|
|
{% for error in form.pixel_spacing.errors %}
|
|
<small>{{ error }}</small>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
<small class="form-text text-muted">Format: row spacing\column spacing</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label class="form-label" for="{{ form.image_orientation.id_for_label }}">
|
|
Image Orientation
|
|
</label>
|
|
{{ form.image_orientation }}
|
|
{% if form.image_orientation.errors %}
|
|
<div class="text-danger">
|
|
{% for error in form.image_orientation.errors %}
|
|
<small>{{ error }}</small>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
<small class="form-text text-muted">DICOM Image Orientation Patient</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Additional Information -->
|
|
{% if form.instance.pk %}
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-info me-2"></i>Additional Information
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="info-group">
|
|
<label class="form-label">Series ID:</label>
|
|
<p class="text-monospace">{{ form.instance.series_id }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="info-group">
|
|
<label class="form-label">Number of Images:</label>
|
|
<p>{{ form.instance.number_of_images|default:0 }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% if form.instance.created_at %}
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="info-group">
|
|
<label class="form-label">Created:</label>
|
|
<p>{{ form.instance.created_at|date:"M d, Y H:i" }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="info-group">
|
|
<label class="form-label">Last Modified:</label>
|
|
<p>{{ form.instance.updated_at|date:"M d, Y H:i" }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Form Actions -->
|
|
<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="form-actions">
|
|
<button type="submit" class="btn btn-primary me-2">
|
|
<i class="fas fa-save me-1"></i>
|
|
{% if form.instance.pk %}Update{% else %}Create{% endif %} Series
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary me-2" onclick="resetForm()">
|
|
<i class="fas fa-undo me-1"></i>Reset
|
|
</button>
|
|
<a href="{% if study %}{% url 'radiology:imaging_series_list' study.pk %}{% else %}{% url 'radiology:imaging_series_list' form.instance.study.pk %}{% endif %}"
|
|
class="btn btn-outline-danger">
|
|
<i class="fas fa-times me-1"></i>Cancel
|
|
</a>
|
|
</div>
|
|
{% if form.instance.pk %}
|
|
<div class="danger-actions">
|
|
<a href="{% url 'radiology:imaging_series_delete' form.instance.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>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Initialize form
|
|
initializeForm();
|
|
|
|
// Auto-generate Series Instance UID if empty
|
|
if (!$('#{{ form.series_instance_uid.id_for_label }}').val()) {
|
|
generateSeriesInstanceUID();
|
|
}
|
|
|
|
// Sync date and time with datetime field
|
|
$('#{{ form.series_date.id_for_label }}, #{{ form.series_time.id_for_label }}').on('change', function() {
|
|
syncDateTime();
|
|
});
|
|
|
|
// Form validation
|
|
$('#series-form').on('submit', function(e) {
|
|
if (!validateForm()) {
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
});
|
|
|
|
function initializeForm() {
|
|
// Add form classes
|
|
$('.form-control, .form-select').addClass('form-control');
|
|
|
|
// Set current date/time if creating new series
|
|
{% if not form.instance.pk %}
|
|
const now = new Date();
|
|
const today = now.toISOString().split('T')[0];
|
|
const currentTime = now.toTimeString().split(' ')[0].substring(0, 5);
|
|
const currentDateTime = now.toISOString().slice(0, 16);
|
|
|
|
if (!$('#{{ form.series_date.id_for_label }}').val()) {
|
|
$('#{{ form.series_date.id_for_label }}').val(today);
|
|
}
|
|
if (!$('#{{ form.series_time.id_for_label }}').val()) {
|
|
$('#{{ form.series_time.id_for_label }}').val(currentTime);
|
|
}
|
|
if (!$('#{{ form.series_datetime.id_for_label }}').val()) {
|
|
$('#{{ form.series_datetime.id_for_label }}').val(currentDateTime);
|
|
}
|
|
{% endif %}
|
|
}
|
|
|
|
function generateSeriesInstanceUID() {
|
|
// Generate a DICOM-compliant Series Instance UID
|
|
const timestamp = Date.now();
|
|
const random = Math.floor(Math.random() * 1000000);
|
|
const uid = `1.2.826.0.1.3680043.8.498.${timestamp}.${random}`;
|
|
$('#{{ form.series_instance_uid.id_for_label }}').val(uid);
|
|
}
|
|
|
|
function syncDateTime() {
|
|
const date = $('#{{ form.series_date.id_for_label }}').val();
|
|
const time = $('#{{ form.series_time.id_for_label }}').val();
|
|
|
|
if (date && time) {
|
|
const datetime = `${date}T${time}`;
|
|
$('#{{ form.series_datetime.id_for_label }}').val(datetime);
|
|
}
|
|
}
|
|
|
|
function validateForm() {
|
|
let isValid = true;
|
|
const requiredFields = [
|
|
'{{ form.series_number.id_for_label }}',
|
|
'{{ form.modality.id_for_label }}',
|
|
'{{ form.series_instance_uid.id_for_label }}',
|
|
'{{ form.series_date.id_for_label }}',
|
|
'{{ form.series_time.id_for_label }}',
|
|
'{{ form.series_datetime.id_for_label }}'
|
|
];
|
|
|
|
// Clear previous error states
|
|
$('.is-invalid').removeClass('is-invalid');
|
|
|
|
// Validate required fields
|
|
requiredFields.forEach(function(fieldId) {
|
|
const field = $('#' + fieldId);
|
|
if (!field.val() || field.val().trim() === '') {
|
|
field.addClass('is-invalid');
|
|
isValid = false;
|
|
}
|
|
});
|
|
|
|
// Validate Series Instance UID format
|
|
const uid = $('#{{ form.series_instance_uid.id_for_label }}').val();
|
|
if (uid && !isValidUID(uid)) {
|
|
$('#{{ form.series_instance_uid.id_for_label }}').addClass('is-invalid');
|
|
alert('Please enter a valid DICOM Series Instance UID');
|
|
isValid = false;
|
|
}
|
|
|
|
// Validate numeric fields
|
|
const numericFields = [
|
|
'{{ form.slice_thickness.id_for_label }}',
|
|
'{{ form.spacing_between_slices.id_for_label }}'
|
|
];
|
|
|
|
numericFields.forEach(function(fieldId) {
|
|
const field = $('#' + fieldId);
|
|
const value = field.val();
|
|
if (value && (isNaN(value) || parseFloat(value) < 0)) {
|
|
field.addClass('is-invalid');
|
|
isValid = false;
|
|
}
|
|
});
|
|
|
|
if (!isValid) {
|
|
alert('Please correct the highlighted fields before submitting.');
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
function isValidUID(uid) {
|
|
// Basic DICOM UID validation
|
|
const uidPattern = /^[0-9]+(\.[0-9]+)*$/;
|
|
return uidPattern.test(uid) && uid.length <= 64;
|
|
}
|
|
|
|
function resetForm() {
|
|
if (confirm('Are you sure you want to reset the form? All unsaved changes will be lost.')) {
|
|
document.getElementById('series-form').reset();
|
|
$('.is-invalid').removeClass('is-invalid');
|
|
initializeForm();
|
|
}
|
|
}
|
|
|
|
// Auto-save functionality (optional)
|
|
let autoSaveTimer;
|
|
function enableAutoSave() {
|
|
$('#series-form input, #series-form select, #series-form textarea').on('input change', function() {
|
|
clearTimeout(autoSaveTimer);
|
|
autoSaveTimer = setTimeout(function() {
|
|
// Auto-save logic here
|
|
console.log('Auto-saving form...');
|
|
}, 30000); // Auto-save after 30 seconds of inactivity
|
|
});
|
|
}
|
|
</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;
|
|
padding: 8px 12px;
|
|
background: #f8f9fa;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-label {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.text-danger {
|
|
color: #dc3545 !important;
|
|
}
|
|
|
|
.is-invalid {
|
|
border-color: #dc3545;
|
|
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
|
}
|
|
|
|
.form-actions .btn {
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.danger-actions {
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* Mobile Responsive */
|
|
@media (max-width: 768px) {
|
|
.d-flex.justify-content-between {
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.form-actions .btn {
|
|
margin-right: 0;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|