631 lines
23 KiB
HTML
631 lines
23 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}{% if form.instance.pk %}Edit{% else %}Create{% endif %} OR Block{% endblock %}
|
|
|
|
{% block css %}
|
|
<style>
|
|
.form-header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border-radius: 0.5rem;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.form-section {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.section-header {
|
|
background: #f8f9fa;
|
|
border-bottom: 1px solid #dee2e6;
|
|
padding: 1rem 1.5rem;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.section-content {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.time-picker-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.duration-display {
|
|
background: #e9ecef;
|
|
border-radius: 0.25rem;
|
|
padding: 0.5rem 1rem;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.team-selection {
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.selected-team-member {
|
|
display: inline-block;
|
|
background: #007bff;
|
|
color: white;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 1rem;
|
|
margin: 0.25rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.preview-section {
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1.5rem;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.conflict-warning {
|
|
background: #fff3cd;
|
|
border: 1px solid #ffeaa7;
|
|
border-radius: 0.25rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.availability-indicator {
|
|
display: inline-block;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
.available { background-color: #28a745; }
|
|
.busy { background-color: #dc3545; }
|
|
.partial { background-color: #ffc107; }
|
|
|
|
@media (max-width: 768px) {
|
|
.form-header {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.time-picker-group {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.section-content {
|
|
padding: 1rem;
|
|
}
|
|
}
|
|
</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 'operating_theatre:dashboard' %}">Operating Theatre</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:or_block_list' %}">Block Schedule</a></li>
|
|
<li class="breadcrumb-item active">{% if form.instance.pk %}Edit{% else %}Create{% endif %} Block</li>
|
|
</ol>
|
|
<h1 class="page-header mb-0">
|
|
<i class="fas fa-calendar-plus me-2"></i>{% if form.instance.pk %}Edit{% else %}Create{% endif %} OR Block
|
|
</h1>
|
|
</div>
|
|
<div class="ms-auto">
|
|
<a href="{% url 'operating_theatre:or_block_list' %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-arrow-left me-1"></i>Back to Schedule
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Form Header -->
|
|
<div class="form-header">
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<h2 class="mb-3">{% if form.instance.pk %}Edit OR Block{% else %}Create New OR Block{% endif %}</h2>
|
|
<p class="mb-0">
|
|
{% if form.instance.pk %}
|
|
Modify the details of this operating room block. Changes will affect all scheduled cases.
|
|
{% else %}
|
|
Create a new operating room block to schedule surgical cases and allocate resources.
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
<div class="col-md-4 text-end">
|
|
<div class="mb-2">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
<span>Block scheduling helps optimize OR utilization</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="post" id="blockForm">
|
|
{% csrf_token %}
|
|
|
|
<!-- Basic Information -->
|
|
<div class="form-section">
|
|
<div class="section-header">
|
|
<i class="fas fa-info-circle me-2"></i>Basic Information
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group mb-3">
|
|
<label class="form-label required">Operating Room</label>
|
|
{{ form.operating_room }}
|
|
{% if form.operating_room.errors %}
|
|
<div class="text-danger small">{{ form.operating_room.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Select the operating room for this block</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group mb-3">
|
|
<label class="form-label required">Block Type</label>
|
|
{{ form.block_type }}
|
|
{% if form.block_type.errors %}
|
|
<div class="text-danger small">{{ form.block_type.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Type of surgical block</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<div class="form-group mb-3">
|
|
<label class="form-label required">Date</label>
|
|
{{ form.date }}
|
|
{% if form.date.errors %}
|
|
<div class="text-danger small">{{ form.date.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<div class="form-group mb-3">
|
|
<label class="form-label required">Time Schedule</label>
|
|
<div class="time-picker-group">
|
|
<div class="flex-fill">
|
|
<label class="form-label small">Start Time</label>
|
|
{{ form.start_time }}
|
|
{% if form.start_time.errors %}
|
|
<div class="text-danger small">{{ form.start_time.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="align-self-end pb-2">
|
|
<i class="fas fa-arrow-right text-muted"></i>
|
|
</div>
|
|
<div class="flex-fill">
|
|
<label class="form-label small">End Time</label>
|
|
{{ form.end_time }}
|
|
{% if form.end_time.errors %}
|
|
<div class="text-danger small">{{ form.end_time.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="align-self-end pb-2">
|
|
<div class="duration-display" id="durationDisplay">
|
|
<i class="fas fa-clock me-1"></i>
|
|
<span id="durationText">0 hours</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Team Assignment -->
|
|
<div class="form-section">
|
|
<div class="section-header">
|
|
<i class="fas fa-users me-2"></i>Team Assignment
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group mb-3">
|
|
<label class="form-label">Primary Surgeon</label>
|
|
{{ form.assigned_surgeon }}
|
|
{% if form.assigned_surgeon.errors %}
|
|
<div class="text-danger small">{{ form.assigned_surgeon.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Lead surgeon for this block</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group mb-3">
|
|
<label class="form-label">Anesthesiologist</label>
|
|
{{ form.assigned_anesthesiologist }}
|
|
{% if form.assigned_anesthesiologist.errors %}
|
|
<div class="text-danger small">{{ form.assigned_anesthesiologist.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Assigned anesthesiologist</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group mb-3">
|
|
<label class="form-label">Nursing Team</label>
|
|
{{ form.assigned_nurses }}
|
|
{% if form.assigned_nurses.errors %}
|
|
<div class="text-danger small">{{ form.assigned_nurses.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Select nursing staff for this block</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Block Configuration -->
|
|
<div class="form-section">
|
|
<div class="section-header">
|
|
<i class="fas fa-cogs me-2"></i>Block Configuration
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group mb-3">
|
|
<label class="form-label">Priority Level</label>
|
|
{{ form.priority }}
|
|
{% if form.priority.errors %}
|
|
<div class="text-danger small">{{ form.priority.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Block priority for scheduling conflicts</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group mb-3">
|
|
<label class="form-label">Maximum Cases</label>
|
|
{{ form.max_cases }}
|
|
{% if form.max_cases.errors %}
|
|
<div class="text-danger small">{{ form.max_cases.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Maximum number of cases for this block</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group mb-3">
|
|
<label class="form-label">Turnover Time (minutes)</label>
|
|
{{ form.turnover_time }}
|
|
{% if form.turnover_time.errors %}
|
|
<div class="text-danger small">{{ form.turnover_time.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Time between cases for room preparation</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group mb-3">
|
|
<div class="form-check mt-4">
|
|
{{ form.allow_emergency_cases }}
|
|
<label class="form-check-label" for="{{ form.allow_emergency_cases.id_for_label }}">
|
|
Allow Emergency Cases
|
|
</label>
|
|
</div>
|
|
<div class="form-text">Allow emergency cases to be scheduled in this block</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notes and Instructions -->
|
|
<div class="form-section">
|
|
<div class="section-header">
|
|
<i class="fas fa-sticky-note me-2"></i>Notes and Instructions
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="form-group mb-3">
|
|
<label class="form-label">Block Notes</label>
|
|
{{ form.notes }}
|
|
{% if form.notes.errors %}
|
|
<div class="text-danger small">{{ form.notes.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Special instructions or notes for this block</div>
|
|
</div>
|
|
|
|
<div class="form-group mb-3">
|
|
<label class="form-label">Setup Requirements</label>
|
|
{{ form.setup_requirements }}
|
|
{% if form.setup_requirements.errors %}
|
|
<div class="text-danger small">{{ form.setup_requirements.errors.0 }}</div>
|
|
{% endif %}
|
|
<div class="form-text">Special equipment or setup requirements</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Availability Check -->
|
|
<div id="availabilityCheck" class="form-section" style="display: none;">
|
|
<div class="section-header">
|
|
<i class="fas fa-calendar-check me-2"></i>Availability Check
|
|
</div>
|
|
<div class="section-content">
|
|
<div id="availabilityResults">
|
|
<!-- Availability results will be loaded here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Conflicts Warning -->
|
|
<div id="conflictsWarning" class="conflict-warning" style="display: none;">
|
|
<h6 class="mb-2">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>Scheduling Conflicts Detected
|
|
</h6>
|
|
<div id="conflictsList">
|
|
<!-- Conflicts will be listed here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Block Preview -->
|
|
<div class="preview-section">
|
|
<h5 class="mb-3">
|
|
<i class="fas fa-eye me-2"></i>Block Preview
|
|
</h5>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-2">
|
|
<strong>Operating Room:</strong> <span id="previewRoom">Not selected</span>
|
|
</div>
|
|
<div class="mb-2">
|
|
<strong>Date & Time:</strong> <span id="previewDateTime">Not set</span>
|
|
</div>
|
|
<div class="mb-2">
|
|
<strong>Duration:</strong> <span id="previewDuration">0 hours</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-2">
|
|
<strong>Block Type:</strong> <span id="previewType">Not selected</span>
|
|
</div>
|
|
<div class="mb-2">
|
|
<strong>Primary Surgeon:</strong> <span id="previewSurgeon">Not assigned</span>
|
|
</div>
|
|
<div class="mb-2">
|
|
<strong>Max Cases:</strong> <span id="previewMaxCases">Not set</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Form Actions -->
|
|
<div class="d-flex justify-content-between mt-4">
|
|
<div>
|
|
<a href="{% url 'operating_theatre:or_block_list' %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-times me-1"></i>Cancel
|
|
</a>
|
|
</div>
|
|
<div>
|
|
<button type="button" class="btn btn-outline-primary me-2" onclick="checkAvailability()">
|
|
<i class="fas fa-search me-1"></i>Check Availability
|
|
</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-save me-1"></i>
|
|
{% if form.instance.pk %}Update Block{% else %}Create Block{% endif %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Initialize form
|
|
updatePreview();
|
|
calculateDuration();
|
|
|
|
// Form change handlers
|
|
$('#blockForm input, #blockForm select, #blockForm textarea').on('change', function() {
|
|
updatePreview();
|
|
if ($(this).attr('name') === 'start_time' || $(this).attr('name') === 'end_time') {
|
|
calculateDuration();
|
|
}
|
|
});
|
|
|
|
// Real-time availability checking
|
|
$('#id_operating_room, #id_date, #id_start_time, #id_end_time').on('change', function() {
|
|
if (allFieldsFilled()) {
|
|
setTimeout(checkAvailability, 500);
|
|
}
|
|
});
|
|
});
|
|
|
|
function calculateDuration() {
|
|
const startTime = $('#id_start_time').val();
|
|
const endTime = $('#id_end_time').val();
|
|
|
|
if (startTime && endTime) {
|
|
const start = new Date('2000-01-01 ' + startTime);
|
|
const end = new Date('2000-01-01 ' + endTime);
|
|
|
|
if (end > start) {
|
|
const diffMs = end - start;
|
|
const diffHours = diffMs / (1000 * 60 * 60);
|
|
const hours = Math.floor(diffHours);
|
|
const minutes = Math.round((diffHours - hours) * 60);
|
|
|
|
let durationText = '';
|
|
if (hours > 0) {
|
|
durationText += hours + ' hour' + (hours !== 1 ? 's' : '');
|
|
}
|
|
if (minutes > 0) {
|
|
if (hours > 0) durationText += ' ';
|
|
durationText += minutes + ' min';
|
|
}
|
|
|
|
$('#durationText').text(durationText || '0 hours');
|
|
$('#previewDuration').text(durationText || '0 hours');
|
|
} else {
|
|
$('#durationText').text('Invalid time range');
|
|
$('#previewDuration').text('Invalid time range');
|
|
}
|
|
} else {
|
|
$('#durationText').text('0 hours');
|
|
$('#previewDuration').text('0 hours');
|
|
}
|
|
}
|
|
|
|
function updatePreview() {
|
|
// Update room
|
|
const roomSelect = $('#id_operating_room');
|
|
const roomText = roomSelect.find('option:selected').text();
|
|
$('#previewRoom').text(roomText !== '---------' ? roomText : 'Not selected');
|
|
|
|
// Update date and time
|
|
const date = $('#id_date').val();
|
|
const startTime = $('#id_start_time').val();
|
|
const endTime = $('#id_end_time').val();
|
|
|
|
if (date && startTime && endTime) {
|
|
const dateObj = new Date(date);
|
|
const dateStr = dateObj.toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
$('#previewDateTime').text(`${dateStr}, ${startTime} - ${endTime}`);
|
|
} else {
|
|
$('#previewDateTime').text('Not set');
|
|
}
|
|
|
|
// Update block type
|
|
const typeSelect = $('#id_block_type');
|
|
const typeText = typeSelect.find('option:selected').text();
|
|
$('#previewType').text(typeText !== '---------' ? typeText : 'Not selected');
|
|
|
|
// Update surgeon
|
|
const surgeonSelect = $('#id_assigned_surgeon');
|
|
const surgeonText = surgeonSelect.find('option:selected').text();
|
|
$('#previewSurgeon').text(surgeonText !== '---------' ? surgeonText : 'Not assigned');
|
|
|
|
// Update max cases
|
|
const maxCases = $('#id_max_cases').val();
|
|
$('#previewMaxCases').text(maxCases || 'Not set');
|
|
}
|
|
|
|
function allFieldsFilled() {
|
|
return $('#id_operating_room').val() &&
|
|
$('#id_date').val() &&
|
|
$('#id_start_time').val() &&
|
|
$('#id_end_time').val();
|
|
}
|
|
|
|
function checkAvailability() {
|
|
if (!allFieldsFilled()) {
|
|
alert('Please fill in operating room, date, start time, and end time first.');
|
|
return;
|
|
}
|
|
|
|
const formData = {
|
|
'operating_room': $('#id_operating_room').val(),
|
|
'date': $('#id_date').val(),
|
|
'start_time': $('#id_start_time').val(),
|
|
'end_time': $('#id_end_time').val(),
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
};
|
|
|
|
$.ajax({
|
|
url: '{% url "operating_theatre:check_availability" %}',
|
|
method: 'POST',
|
|
data: formData,
|
|
success: function(response) {
|
|
displayAvailabilityResults(response);
|
|
},
|
|
error: function() {
|
|
alert('Error checking availability');
|
|
}
|
|
});
|
|
}
|
|
|
|
function displayAvailabilityResults(data) {
|
|
const availabilitySection = $('#availabilityCheck');
|
|
const resultsDiv = $('#availabilityResults');
|
|
const conflictsWarning = $('#conflictsWarning');
|
|
const conflictsList = $('#conflictsList');
|
|
|
|
// Show availability section
|
|
availabilitySection.show();
|
|
|
|
// Clear previous results
|
|
resultsDiv.empty();
|
|
conflictsList.empty();
|
|
|
|
// Room availability
|
|
const roomStatus = data.room_available ? 'available' : 'busy';
|
|
const roomIndicator = `<span class="availability-indicator ${roomStatus}"></span>`;
|
|
resultsDiv.append(`
|
|
<div class="mb-3">
|
|
<h6>Operating Room Availability</h6>
|
|
<div>${roomIndicator} ${data.room_available ? 'Available' : 'Not Available'}</div>
|
|
</div>
|
|
`);
|
|
|
|
// Team availability
|
|
if (data.team_availability) {
|
|
resultsDiv.append('<div class="mb-3"><h6>Team Availability</h6></div>');
|
|
|
|
Object.keys(data.team_availability).forEach(function(member) {
|
|
const availability = data.team_availability[member];
|
|
const status = availability.available ? 'available' : 'busy';
|
|
const indicator = `<span class="availability-indicator ${status}"></span>`;
|
|
resultsDiv.append(`
|
|
<div class="mb-1">
|
|
${indicator} ${member}: ${availability.available ? 'Available' : availability.reason}
|
|
</div>
|
|
`);
|
|
});
|
|
}
|
|
|
|
// Show conflicts if any
|
|
if (data.conflicts && data.conflicts.length > 0) {
|
|
conflictsWarning.show();
|
|
data.conflicts.forEach(function(conflict) {
|
|
conflictsList.append(`
|
|
<div class="mb-1">
|
|
<i class="fas fa-exclamation-circle me-1"></i>
|
|
${conflict.message}
|
|
</div>
|
|
`);
|
|
});
|
|
} else {
|
|
conflictsWarning.hide();
|
|
}
|
|
}
|
|
|
|
// Form validation
|
|
$('#blockForm').on('submit', function(e) {
|
|
const startTime = $('#id_start_time').val();
|
|
const endTime = $('#id_end_time').val();
|
|
|
|
if (startTime && endTime) {
|
|
const start = new Date('2000-01-01 ' + startTime);
|
|
const end = new Date('2000-01-01 ' + endTime);
|
|
|
|
if (end <= start) {
|
|
e.preventDefault();
|
|
alert('End time must be after start time.');
|
|
return false;
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|