2025-10-06 15:25:37 +03:00

1769 lines
62 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Operating Theatre app views with healthcare-focused CRUD operations.
Implements appropriate access patterns for surgical and OR management workflows.
"""
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views import View
from django.views.generic import (
ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView
)
from django.db.models import Q, Count, Avg, F, DurationField, ExpressionWrapper
from django.contrib import messages
from django.urls import reverse_lazy, reverse
from django.core.paginator import Paginator
from django.template.loader import render_to_string
from django.http import JsonResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
from datetime import datetime, timedelta, date
import json
from django.views.decorators.http import require_POST, require_GET, require_http_methods
from django.utils import timezone
from django.utils.dateparse import parse_datetime, parse_date, parse_time
from core.utils import AuditLogger
from .models import *
from .forms import *
class OperatingTheatreDashboardView(LoginRequiredMixin, TemplateView):
template_name = 'operating_theatre/dashboard.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = self.request.user.tenant
today = timezone.now().date()
# --- Stats ---
context.update({
'total_rooms': OperatingRoom.objects.filter(
tenant=tenant, is_active=True
).count(),
'rooms_available': OperatingRoom.objects.filter(
tenant=tenant, is_active=True, status='AVAILABLE'
).count(),
'rooms_in_use': OperatingRoom.objects.filter(
tenant=tenant, is_active=True, status='OCCUPIED' # was IN_USE
).count(),
'rooms_maintenance': OperatingRoom.objects.filter(
tenant=tenant, is_active=True, status='MAINTENANCE'
).count(),
'cases_today': SurgicalCase.objects.filter(
or_block__operating_room__tenant=tenant, # was admission__tenant
scheduled_start__date=today
).count(),
'cases_in_progress': SurgicalCase.objects.filter(
or_block__operating_room__tenant=tenant, # was admission__tenant
status='IN_PROGRESS'
).count(),
'cases_completed_today': SurgicalCase.objects.filter(
or_block__operating_room__tenant=tenant, # was admission__tenant
actual_end__date=today,
status='COMPLETED'
).count(),
'emergency_cases_today': SurgicalCase.objects.filter(
or_block__operating_room__tenant=tenant, # was admission__tenant
scheduled_start__date=today,
case_type='EMERGENCY'
).count(),
'blocks_today': ORBlock.objects.filter(
operating_room__tenant=tenant,
date=today
).count(),
'equipment_in_use': EquipmentUsage.objects.filter(
surgical_case__or_block__operating_room__tenant=tenant
# add a real "in use" condition if you have one (e.g., end_time__isnull=True)
).count(),
'notes_pending': SurgicalNote.objects.filter(
surgical_case__or_block__operating_room__tenant=tenant,
status='DRAFT'
).count(),
})
# --- Recent cases ---
context['recent_cases'] = (
SurgicalCase.objects
.filter(or_block__operating_room__tenant=tenant)
.select_related('patient', 'primary_surgeon', 'or_block', 'or_block__operating_room')
.order_by('-scheduled_start')[:10]
)
# --- Todays schedule ---
context['todays_schedule'] = (
SurgicalCase.objects
.filter(or_block__operating_room__tenant=tenant, scheduled_start__date=today)
.select_related('patient', 'primary_surgeon', 'or_block', 'or_block__operating_room')
.order_by('scheduled_start')
)
# --- Room utilization (cases today per room) ---
context['room_utilization'] = (
OperatingRoom.objects
.filter(tenant=tenant, is_active=True)
.annotate(
cases_today=Count(
'or_blocks__surgical_cases',
filter=Q(or_blocks__surgical_cases__scheduled_start__date=today)
)
)
.order_by('room_number')
)
return context
class OperatingRoomListView(LoginRequiredMixin, ListView):
"""
List all operating rooms with filtering and search.
"""
model = OperatingRoom
template_name = 'operating_theatre/rooms/operating_room_list.html'
context_object_name = 'operating_rooms'
paginate_by = 25
def get_queryset(self):
queryset = OperatingRoom.objects.filter(tenant=self.request.user.tenant)
# Search functionality
search = self.request.GET.get('search')
if search:
queryset = queryset.filter(
Q(room_number__icontains=search) |
Q(room_name__icontains=search) |
Q(location__icontains=search)
)
# Filter by room type
room_type = self.request.GET.get('room_type')
if room_type:
queryset = queryset.filter(room_type=room_type)
# Filter by status
status = self.request.GET.get('status')
if status:
queryset = queryset.filter(status=status)
# Filter by floor
floor = self.request.GET.get('floor')
if floor:
queryset = queryset.filter(floor=floor)
# Filter by active status
active_only = self.request.GET.get('active_only')
if active_only:
queryset = queryset.filter(is_active=True)
return queryset.order_by('room_number')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'room_types': OperatingRoom._meta.get_field('room_type').choices,
'statuses': OperatingRoom._meta.get_field('status').choices,
'search_query': self.request.GET.get('search', ''),
})
return context
class OperatingRoomDetailView(LoginRequiredMixin, DetailView):
"""
Display detailed information about an operating room.
"""
model = OperatingRoom
template_name = 'operating_theatre/rooms/operating_room_detail.html'
context_object_name = 'operating_room'
def get_queryset(self):
# tenant scoping
return OperatingRoom.objects.filter(tenant=self.request.user.tenant)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
operating_room = self.object
today = timezone.now().date()
# Base QS for cases in this room
room_cases = (
SurgicalCase.objects
.filter(or_block__operating_room=operating_room)
.select_related('patient', 'primary_surgeon', 'or_block', 'or_block__operating_room')
)
# Today's cases
context['todays_cases'] = (
room_cases
.filter(scheduled_start__date=today)
.order_by('scheduled_start')
)
# Recent cases
context['recent_cases'] = room_cases.order_by('-scheduled_start')[:10]
# Equipment usage for this room (scope via case -> block -> room)
context['equipment_usage'] = (
EquipmentUsage.objects
.filter(surgical_case__or_block__operating_room=operating_room)
.select_related('surgical_case')
.order_by('-start_time')[:10]
)
# Average duration: (actual_end - actual_start) over completed cases
completed_cases = room_cases.filter(actual_start__isnull=False, actual_end__isnull=False)
duration_expr = ExpressionWrapper(
F('actual_end') - F('actual_start'),
output_field=DurationField()
)
avg_duration = completed_cases.aggregate(avg_duration=Avg(duration_expr))['avg_duration']
# Room statistics
now = timezone.now()
context['room_stats'] = {
'total_cases': room_cases.count(),
'cases_this_month': room_cases.filter(
scheduled_start__year=now.year,
scheduled_start__month=now.month
).count(),
'average_case_duration': avg_duration, # a datetime.timedelta or None
}
return context
class OperatingRoomCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
Create a new operating room (fields matched to the form/template).
"""
model = OperatingRoom
form_class = OperatingRoomForm
template_name = 'operating_theatre/rooms/operating_room_form.html'
permission_required = 'operating_theatre.add_operatingroom'
success_url = reverse_lazy('operating_theatre:operating_room_list')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['tenant'] = self.request.user.tenant
return kwargs
def post(self, request, *args, **kwargs):
"""
Support 'Save as Draft' AJAX click from the template.
We return a JSON success without persisting anything (no draft model).
If you later want true draft saving, add a Draft model or a flag.
"""
if request.POST.get('save_draft') == 'true':
return JsonResponse({'success': True})
return super().post(request, *args, **kwargs)
def form_valid(self, form):
"""
Attach tenant (and created_by if desired) then save.
"""
form.instance.tenant = self.request.user.tenant
if hasattr(form.instance, 'created_by') and not form.instance.created_by_id:
form.instance.created_by = self.request.user
response = super().form_valid(form)
# Log the action
try:
AuditLogger.log_event(
user=self.request.user,
action='OPERATING_ROOM_CREATED',
model='OperatingRoom',
object_id=str(self.object.pk),
details={
'room_number': self.object.room_number,
'room_name': self.object.room_name,
'room_type': self.object.room_type,
},
)
except Exception:
# keep UX smooth even if audit logging fails
pass
messages.success(self.request, f'Operating room "{self.object.room_number}" created successfully.')
return response
class OperatingRoomUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
Update an existing operating room.
"""
model = OperatingRoom
form_class = OperatingRoomForm
template_name = 'operating_theatre/rooms/operating_room_form.html'
permission_required = 'operating_theatre.change_operatingroom'
def get_queryset(self):
return OperatingRoom.objects.filter(tenant=self.request.user.tenant)
def get_success_url(self):
return reverse('operating_theatre:operating_room_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
response = super().form_valid(form)
# Log the action
AuditLogger.log_action(
user=self.request.user,
action='OPERATING_ROOM_UPDATED',
model='OperatingRoom',
object_id=str(self.object.id),
details={
'room_number': self.object.room_number,
'changes': form.changed_data
}
)
messages.success(self.request, f'Operating room "{self.object.room_number}" updated successfully.')
return response
class OperatingRoomDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
"""
Delete an operating room (soft delete by deactivating).
"""
model = OperatingRoom
template_name = 'operating_theatre/rooms/operating_room_confirm_delete.html'
permission_required = 'operating_theatre.delete_operatingroom'
success_url = reverse_lazy('operating_theatre:operating_room_list')
def get_queryset(self):
return OperatingRoom.objects.filter(tenant=self.request.user.tenant)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
# Check if room has active cases
active_cases = self.object.surgical_cases.filter(
status__in=['SCHEDULED', 'IN_PROGRESS']
).count()
if active_cases > 0:
messages.error(
request,
f'Cannot deactivate room "{self.object.room_number}" - it has {active_cases} active cases.'
)
return redirect('operating_theatre:operating_room_detail', pk=self.object.pk)
# Soft delete by deactivating
self.object.is_active = False
self.object.status = 'OUT_OF_SERVICE'
self.object.save()
# Log the action
AuditLogger.log_event(
user=request.user,
action='OPERATING_ROOM_DEACTIVATED',
model='OperatingRoom',
object_id=str(self.object.id),
details={'room_number': self.object.room_number}
)
messages.success(request, f'Operating room "{self.object.room_number}" deactivated successfully.')
return redirect(self.success_url)
class SurgicalNoteTemplateListView(LoginRequiredMixin, ListView):
"""
List all surgical note templates with filtering and search.
"""
model = SurgicalNoteTemplate
template_name = 'operating_theatre/templates/surgical_note_template_list.html'
context_object_name = 'surgical_note_templates'
paginate_by = 25
def get_queryset(self):
queryset = SurgicalNoteTemplate.objects.filter(tenant=self.request.user.tenant)
# Search functionality
search = self.request.GET.get('search')
if search:
queryset = queryset.filter(
Q(template_name__icontains=search) |
Q(procedure_type__icontains=search) |
Q(specialty__icontains=search)
)
# Filter by specialty
specialty = self.request.GET.get('specialty')
if specialty:
queryset = queryset.filter(specialty=specialty)
# Filter by active status
active_only = self.request.GET.get('active_only')
if active_only:
queryset = queryset.filter(is_active=True)
return queryset.order_by('name')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'specialties': SurgicalNoteTemplate._meta.get_field('specialty').choices,
'search_query': self.request.GET.get('search', ''),
})
return context
class SurgicalNoteTemplateDetailView(LoginRequiredMixin, DetailView):
"""
Display detailed information about a surgical note template.
"""
model = SurgicalNoteTemplate
template_name = 'operating_theatre/templates/surgical_note_template_detail.html'
context_object_name = 'surgical_note_template'
def get_queryset(self):
return SurgicalNoteTemplate.objects.filter(tenant=self.request.user.tenant)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
template = self.object
# Get recent notes using this template
context['recent_notes'] = SurgicalNote.objects.filter(
template=template,
tenant=self.request.user.tenant
).select_related('surgical_case__patient').order_by('-created_at')[:10]
return context
class SurgicalNoteTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
Create a new surgical note template.
"""
model = SurgicalNoteTemplate
form_class = SurgicalNoteTemplateForm
template_name = 'operating_theatre/templates/surgical_note_template_form.html'
permission_required = 'operating_theatre.add_surgicalnotetemplate'
success_url = reverse_lazy('operating_theatre:surgical_note_template_list')
def form_valid(self, form):
form.instance.tenant = self.request.user.tenant
form.instance.created_by = self.request.user
response = super().form_valid(form)
# Log the action
AuditLogger.log_event(
user=self.request.user,
action='SURGICAL_NOTE_TEMPLATE_CREATED',
model='SurgicalNoteTemplate',
object_id=str(self.object.id),
details={
'template_name': self.object.template_name,
'specialty': self.object.specialty
}
)
messages.success(self.request, f'Surgical note template "{self.object.template_name}" created successfully.')
return response
class SurgicalNoteTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
Update an existing surgical note template.
"""
model = SurgicalNoteTemplate
form_class = SurgicalNoteTemplateForm
template_name = 'operating_theatre/templates/surgical_note_template_form.html'
permission_required = 'operating_theatre.change_surgicalnotetemplate'
def get_queryset(self):
return SurgicalNoteTemplate.objects.filter(tenant=self.request.user.tenant)
def get_success_url(self):
return reverse('operating_theatre:surgical_note_template_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
response = super().form_valid(form)
# Log the action
AuditLogger.log_event(
user=self.request.user,
action='SURGICAL_NOTE_TEMPLATE_UPDATED',
model='SurgicalNoteTemplate',
object_id=str(self.object.id),
details={
'template_name': self.object.template_name,
'changes': form.changed_data
}
)
messages.success(self.request, f'Surgical note template "{self.object.template_name}" updated successfully.')
return response
class SurgicalNoteTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
"""
Delete a surgical note template (soft delete by deactivating).
"""
model = SurgicalNoteTemplate
template_name = 'operating_theatre/templates/surgical_note_template_confirm_delete.html'
permission_required = 'operating_theatre.delete_surgicalnotetemplate'
success_url = reverse_lazy('operating_theatre:surgical_note_template_list')
def get_queryset(self):
return SurgicalNoteTemplate.objects.filter(tenant=self.request.user.tenant)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
# Soft delete by deactivating
self.object.is_active = False
self.object.save()
# Log the action
AuditLogger.log_event(
user=request.user,
action='SURGICAL_NOTE_TEMPLATE_DEACTIVATED',
model='SurgicalNoteTemplate',
object_id=str(self.object.id),
details={'template_name': self.object.template_name}
)
messages.success(request, f'Surgical note template "{self.object.template_name}" deactivated successfully.')
return redirect(self.success_url)
class ORBlockListView(LoginRequiredMixin, ListView):
"""
List all OR blocks with filtering and search.
"""
model = ORBlock
template_name = 'operating_theatre/blocks/block_list.html'
context_object_name = 'blocks'
paginate_by = 25
def get_queryset(self):
queryset = ORBlock.objects.filter(operating_room__tenant=self.request.user.tenant)
# Filter by date range
date_from = self.request.GET.get('date_from')
date_to = self.request.GET.get('date_to')
if date_from:
queryset = queryset.filter(date__gte=date_from)
if date_to:
queryset = queryset.filter(date__lte=date_to)
# Filter by surgeon
surgeon_id = self.request.GET.get('surgeon')
if surgeon_id:
queryset = queryset.filter(surgeon_id=surgeon_id)
# Filter by operating room
room_id = self.request.GET.get('room')
if room_id:
queryset = queryset.filter(operating_room_id=room_id)
return queryset.select_related(
'operating_room', 'primary_surgeon',
).order_by('-date', 'start_time')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'operating_rooms': OperatingRoom.objects.filter(
tenant=self.request.user.tenant,
is_active=True
).order_by('room_number'),
})
return context
class ORBlockDetailView(LoginRequiredMixin, DetailView):
"""
Display detailed information about an OR block.
"""
model = ORBlock
template_name = 'operating_theatre/blocks/block_detail.html'
# context_object_name = 'block'
def get_queryset(self):
return ORBlock.objects.filter(operating_room__tenant=self.request.user.tenant)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
or_block = self.object
# Get cases scheduled in this block
scheduled_cases = SurgicalCase.objects.filter(
or_block__operating_room=or_block.operating_room,
scheduled_start__date=or_block.date,
scheduled_start__time__gte=or_block.start_time,
scheduled_start__time__lt=or_block.end_time,
or_block__operating_room__tenant=self.request.user.tenant
).select_related('patient', 'primary_surgeon').order_by('scheduled_start')
context['scheduled_cases'] = scheduled_cases
# Calculate utilization
total_block_minutes = (
datetime.combine(timezone.now().date(), or_block.end_time) -
datetime.combine(timezone.now().date(), or_block.start_time)
).total_seconds() / 60
used_minutes = 0
for case in scheduled_cases:
if case.estimated_duration:
used_minutes += case.estimated_duration
context['utilization_percentage'] = (
(used_minutes / total_block_minutes) * 100 if total_block_minutes > 0 else 0
)
context['total_block_minutes'] = total_block_minutes
return context
class ORBlockCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
Create a new OR block.
"""
model = ORBlock
form_class = ORBlockForm
template_name = 'operating_theatre/blocks/block_form.html'
permission_required = 'operating_theatre.add_orblock'
success_url = reverse_lazy('operating_theatre:or_block_list')
def form_valid(self, form):
form.instance.tenant = self.request.user.tenant
response = super().form_valid(form)
# Log the action
AuditLogger.log_event(
user=self.request.user,
action='OR_BLOCK_CREATED',
model='ORBlock',
object_id=str(self.object.id),
details={
'date': str(self.object.date),
'operating_room': self.object.operating_room.room_number,
'surgeon': f"{self.object.surgeon.first_name} {self.object.surgeon.last_name}"
}
)
messages.success(self.request, 'OR block created successfully.')
return response
class ORBlockUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
Update an OR block (limited to notes and time adjustments).
"""
model = ORBlock
fields = ['start_time', 'end_time', 'notes'] # Restricted fields
template_name = 'operating_theatre/or_block_form.html'
permission_required = 'operating_theatre.change_orblock'
def get_queryset(self):
return ORBlock.objects.filter(tenant=self.request.user.tenant)
def get_success_url(self):
return reverse('operating_theatre:or_block_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
response = super().form_valid(form)
# Log the action
AuditLogger.log_action(
user=self.request.user,
action='OR_BLOCK_UPDATED',
model='ORBlock',
object_id=str(self.object.id),
details={
'date': str(self.object.date),
'changes': form.changed_data
}
)
messages.success(self.request, 'OR block updated successfully.')
return response
class SurgicalCaseListView(LoginRequiredMixin, ListView):
"""
List all surgical cases with filtering and search.
"""
model = SurgicalCase
template_name = 'operating_theatre/cases/surgical_case_list.html'
context_object_name = 'surgical_cases'
paginate_by = 25
def get_queryset(self):
tenant = self.request.user.tenant
qs = SurgicalCase.objects.filter(admission__tenant=tenant)
# Search
search = self.request.GET.get('search', '').strip()
if search:
# You can keep mrn fields if PatientProfile has them
name_q = Q(patient__first_name__icontains=search) | Q(patient__last_name__icontains=search)
mrn_q = Q(patient__mrn__icontains=search)
proc_q = Q(primary_procedure__icontains=search)
qs = qs.filter(name_q | mrn_q | proc_q)
# Status
status = self.request.GET.get('status')
if status:
qs = qs.filter(status=status)
# Case type (your "priority" select)
case_type = self.request.GET.get('priority')
if case_type:
qs = qs.filter(case_type=case_type)
# Surgeon
surgeon_id = self.request.GET.get('surgeon')
if surgeon_id:
qs = qs.filter(primary_surgeon_id=surgeon_id)
# Room (goes through ORBlock -> OperatingRoom)
room_id = self.request.GET.get('room')
if room_id:
qs = qs.filter(or_block__operating_room_id=room_id)
# Date range (scheduled_start is a DateTimeField)
date_from = self.request.GET.get('date_from')
date_to = self.request.GET.get('date_to')
if date_from:
qs = qs.filter(scheduled_start__date__gte=date_from)
if date_to:
qs = qs.filter(scheduled_start__date__lte=date_to)
return qs.select_related('patient', 'primary_surgeon', 'or_block__operating_room').order_by('-scheduled_start')
def get_context_data(self, **kwargs):
from django.contrib.auth import get_user_model
User = get_user_model()
context = super().get_context_data(**kwargs)
tenant = self.request.user.tenant
context.update({
'statuses': SurgicalCase.CaseStatus.choices,
'case_types': SurgicalCase.CaseType.choices, # renamed for clarity
'operating_rooms': OperatingRoom.objects.filter(tenant=tenant, is_active=True).order_by('room_number'),
'surgeons': User.objects.filter(tenant=tenant, is_active=True).order_by('first_name', 'last_name'),
})
return context
class SurgicalCaseDetailView(LoginRequiredMixin, DetailView):
"""
Display detailed information about a surgical case.
"""
model = SurgicalCase
template_name = 'operating_theatre/cases/surgical_case_detail.html'
context_object_name = 'surgical_case'
def get_queryset(self):
return SurgicalCase.objects.filter(or_block__operating_room__tenant=self.request.user.tenant)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
surgical_case = self.object
# Get surgical notes for this case
# context['surgical_notes'] = surgical_case.surgical_notes
# Get equipment usage for this case
context['equipment_usage'] = EquipmentUsage.objects.filter(
surgical_case=surgical_case,
surgical_case__or_block__operating_room__tenant=self.request.user.tenant
).order_by('-start_time')
# Calculate actual duration if case is completed
if surgical_case.actual_start and surgical_case.actual_end:
context['actual_duration'] = surgical_case.actual_end - surgical_case.actual_start
return context
class SurgicalCaseCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
Create a new surgical case.
"""
model = SurgicalCase
form_class = SurgicalCaseForm
template_name = 'operating_theatre/cases/surgical_case_form.html'
permission_required = 'operating_theatre.add_surgicalcase'
success_url = reverse_lazy('operating_theatre:surgical_case_list')
def form_valid(self, form):
form.instance.tenant = self.request.user.tenant
response = super().form_valid(form)
# Log the action
AuditLogger.log_action(
user=self.request.user,
action='SURGICAL_CASE_CREATED',
model='SurgicalCase',
object_id=str(self.object.case_id),
details={
'patient_name': f"{self.object.patient.first_name} {self.object.patient.last_name}",
'procedure_name': self.object.procedure_name,
'priority': self.object.priority
}
)
messages.success(self.request, 'Surgical case created successfully.')
return response
class SurgicalCaseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
Update surgical case (limited to status and notes after surgery starts).
"""
model = SurgicalCase
template_name = 'operating_theatre/cases/surgical_case_form.html'
permission_required = 'operating_theatre.change_surgicalcase'
def get_queryset(self):
return SurgicalCase.objects.filter(or_block__operating_room__tenant=self.request.user.tenant)
def get_form_class(self):
# Limit fields based on case status
if self.object.status in ['IN_PROGRESS', 'COMPLETED']:
# Limited fields for cases that have started
class RestrictedSurgicalCaseForm(SurgicalCaseForm):
class Meta(SurgicalCaseForm.Meta):
fields = ['status', 'complications']
return RestrictedSurgicalCaseForm
else:
return SurgicalCaseForm
def get_success_url(self):
return reverse('operating_theatre:surgical_case_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
response = super().form_valid(form)
# Log the action
AuditLogger.log_event(
user=self.request.user,
action='SURGICAL_CASE_UPDATED',
model='SurgicalCase',
object_id=str(self.object.case_id),
details={
'patient_name': f"{self.object.patient.first_name} {self.object.patient.last_name}",
'changes': form.changed_data
}
)
messages.success(self.request, 'Surgical case updated successfully.')
return response
class SurgicalNoteListView(LoginRequiredMixin, ListView):
"""
List all surgical notes with filtering and search.
"""
model = SurgicalNote
template_name = 'operating_theatre/notes/surgical_note_list.html'
context_object_name = 'notes'
paginate_by = 25
def get_queryset(self):
queryset = SurgicalNote.objects.filter(surgeon__tenant=self.request.user.tenant)
# Search functionality
search = self.request.GET.get('search')
if search:
queryset = queryset.filter(
Q(surgical_case__patient__first_name__icontains=search) |
Q(surgical_case__patient__last_name__icontains=search) |
Q(surgical_case__patient__mrn__icontains=search) |
Q(surgical_case__procedure_name__icontains=search) |
Q(note_content__icontains=search)
)
# Filter by note type
note_type = self.request.GET.get('note_type')
if note_type:
queryset = queryset.filter(note_type=note_type)
# Filter by status
status = self.request.GET.get('status')
if status:
queryset = queryset.filter(status=status)
# Filter by surgeon
surgeon_id = self.request.GET.get('surgeon')
if surgeon_id:
queryset = queryset.filter(surgeon_id=surgeon_id)
return queryset.select_related(
'surgical_case__patient', 'surgeon', 'template_used'
).order_by('-created_at')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
# 'note_types': SurgicalNote._meta.get_field('note_type').choices,
'statuses': SurgicalNote.NoteStatus.choices,
})
return context
class SurgicalNoteDetailView(LoginRequiredMixin, DetailView):
"""
Display detailed information about a surgical note.
"""
model = SurgicalNote
template_name = 'operating_theatre/notes/surgical_note_detail.html'
context_object_name = 'note'
def get_queryset(self):
return SurgicalNote.objects.filter(surgeon__tenant=self.request.user.tenant)
class SurgicalNoteCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
Create a new surgical note.
"""
model = SurgicalNote
form_class = SurgicalNoteForm
template_name = 'operating_theatre/notes/surgical_note_form.html'
permission_required = 'operating_theatre.add_surgicalnote'
success_url = reverse_lazy('operating_theatre:surgical_note_list')
def form_valid(self, form):
form.instance.tenant = self.request.user.tenant
form.instance.surgeon = self.request.user
response = super().form_valid(form)
# Log the action
AuditLogger.log_event(
user=self.request.user,
action='SURGICAL_NOTE_CREATED',
model='SurgicalNote',
object_id=str(self.object.note_id),
details={
'patient_name': f"{self.object.surgical_case.patient.first_name} {self.object.surgical_case.patient.last_name}",
'note_type': self.object.note_type
}
)
messages.success(self.request, 'Surgical note created successfully.')
return response
class EquipmentUsageListView(LoginRequiredMixin, ListView):
"""
List all equipment usage records with filtering and search.
"""
model = EquipmentUsage
template_name = 'operating_theatre/equipment/equipment_list.html'
context_object_name = 'equipment_list'
paginate_by = 25
def get_queryset(self):
queryset = EquipmentUsage.objects.filter(surgical_case__admission__tenant=self.request.user.tenant)
# Filter by equipment type
equipment_type = self.request.GET.get('equipment_type')
if equipment_type:
queryset = queryset.filter(equipment_type=equipment_type)
# Filter by status
status = self.request.GET.get('status')
if status:
queryset = queryset.filter(status=status)
# Filter by operating room
room_id = self.request.GET.get('room')
if room_id:
queryset = queryset.filter(operating_room_id=room_id)
# Filter by date range
date_from = self.request.GET.get('date_from')
date_to = self.request.GET.get('date_to')
if date_from:
queryset = queryset.filter(start_time__date__gte=date_from)
if date_to:
queryset = queryset.filter(start_time__date__lte=date_to)
return queryset.select_related(
'surgical_case__or_block__operating_room', 'surgical_case__patient'
).order_by('-start_time')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'equipment_types': EquipmentUsage.EquipmentType.choices,
# 'statuses': EquipmentUsage._meta.get_field('status').choices,
'operating_rooms': OperatingRoom.objects.filter(
tenant=self.request.user.tenant,
is_active=True
).order_by('room_number'),
})
return context
class EquipmentUsageDetailView(LoginRequiredMixin, DetailView):
"""
Display detailed information about equipment usage.
"""
model = EquipmentUsage
template_name = 'operating_theatre/equipment/equipment_detail.html'
context_object_name = 'equipment'
def get_queryset(self):
tenant = self.request.user.tenant
return EquipmentUsage.objects.filter(surgical_case__patient__tenant=tenant)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
equipment_usage = self.object
# Calculate usage duration if ended
if equipment_usage.end_time:
context['usage_duration'] = equipment_usage.end_time - equipment_usage.start_time
return context
class EquipmentUsageCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
Create a new equipment usage record.
"""
model = EquipmentUsage
form_class = EquipmentUsageForm
template_name = 'operating_theatre/equipment/equipment_form.html'
permission_required = 'operating_theatre.add_equipmentusage'
success_url = reverse_lazy('operating_theatre:equipment_usage_list')
def form_valid(self, form):
form.instance.tenant = self.request.user.tenant
response = super().form_valid(form)
# Log the action
AuditLogger.log_action(
user=self.request.user,
action='EQUIPMENT_USAGE_CREATED',
model='EquipmentUsage',
object_id=str(self.object.id),
details={
'equipment_name': self.object.equipment_name,
'equipment_type': self.object.equipment_type,
'operating_room': self.object.operating_room.room_number
}
)
messages.success(self.request, 'Equipment usage record created successfully.')
return response
class EquipmentUsageUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
Update equipment usage record (limited to status and end time).
"""
model = EquipmentUsage
fields = ['status', 'end_time', 'notes'] # Restricted fields
template_name = 'operating_theatre/equipment/equipment_form.html'
permission_required = 'operating_theatre.change_equipmentusage'
def get_queryset(self):
return EquipmentUsage.objects.filter(tenant=self.request.user.tenant)
def get_success_url(self):
return reverse('operating_theatre:equipment_usage_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
response = super().form_valid(form)
# Log the action
AuditLogger.log_event(
user=self.request.user,
action='EQUIPMENT_USAGE_UPDATED',
model='EquipmentUsage',
object_id=str(self.object.id),
details={
'equipment_name': self.object.equipment_name,
'changes': form.changed_data
}
)
messages.success(self.request, 'Equipment usage record updated successfully.')
return response
class StartCaseView(LoginRequiredMixin, PermissionRequiredMixin, View):
"""
Mark a scheduled case as IN_PROGRESS and set room status to OCCUPIED.
"""
permission_required = 'operating_theatre.change_surgicalcase'
def post(self, request, pk):
case = get_object_or_404(SurgicalCase, pk=pk, admission__tenant=request.user.tenant)
if case.status != 'SCHEDULED':
messages.error(request, 'Only scheduled cases can be started.')
return redirect('operating_theatre:surgical_case_list')
case.status = 'IN_PROGRESS'
if not case.actual_start:
case.actual_start = timezone.now()
case.save()
# Set room status
if case.or_block and case.or_block.operating_room:
room = case.or_block.operating_room
room.status = 'OCCUPIED'
room.save(update_fields=['status'])
messages.success(request, f'Case {case.case_number} started.')
return redirect('operating_theatre:surgical_case_list')
class CompleteCaseView(LoginRequiredMixin, PermissionRequiredMixin, View):
"""
Mark an in-progress case as COMPLETED and set room to CLEANING.
"""
permission_required = 'operating_theatre.change_surgicalcase'
def post(self, request, pk):
case = get_object_or_404(SurgicalCase, pk=pk, admission__tenant=request.user.tenant)
if case.status != 'IN_PROGRESS':
messages.error(request, 'Only in-progress cases can be completed.')
return redirect('operating_theatre:surgical_case_list')
case.status = 'COMPLETED'
if not case.actual_end:
case.actual_end = timezone.now()
case.save()
# Set room status
if case.or_block and case.or_block.operating_room:
room = case.or_block.operating_room
room.status = 'CLEANING'
room.save(update_fields=['status'])
messages.success(request, f'Case {case.case_number} marked as completed.')
return redirect('operating_theatre:surgical_case_list')
class SurgicalNotePreviewView(LoginRequiredMixin, View):
"""
Preview a surgical note before finalizing or signing.
Allows users to review the note content in a read-only format.
"""
model = SurgicalNote
template_name = 'operating_theatre/surgical_note_preview.html'
context_object_name = 'surgical_note'
def get_queryset(self):
return SurgicalNote.objects.filter(tenant=self.request.user.tenant)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
surgical_note = self.object
# Add surgical case information
context['surgical_case'] = surgical_note.surgical_case
# Add template information if available
if surgical_note.template:
context['template'] = surgical_note.template
# Check if user can sign the note
context['can_sign'] = (
surgical_note.status == 'DRAFT' and
(surgical_note.created_by == self.request.user or
self.request.user.has_perm('operating_theatre.change_surgicalnote'))
)
# Check if user can edit the note
context['can_edit'] = (
surgical_note.status == 'DRAFT' and
surgical_note.created_by == self.request.user
)
# Add signature information
context['signatures'] = surgical_note.signatures.all().order_by('signed_at')
# Add related equipment usage
if surgical_note.surgical_case:
context['equipment_usage'] = EquipmentUsage.objects.filter(
surgical_case=surgical_note.surgical_case,
tenant=self.request.user.tenant
).order_by('start_time')
return context
@login_required
def sign_note(request, note_id):
"""
Sign a surgical note.
"""
if request.method == 'POST':
note = get_object_or_404(
SurgicalNote,
id=note_id,
tenant=request.user.tenant
)
# Only allow signing if note is in draft status
if note.status != 'DRAFT':
messages.error(request, 'Only draft notes can be signed.')
return redirect('operating_theatre:surgical_note_detail', pk=note.pk)
note.status = 'SIGNED'
note.signed_datetime = timezone.now()
note.signed_by = request.user
note.save()
# Log the action
AuditLogger.log_event(
tenant=request.user.tenant,
user=request.user,
action='SURGICAL_NOTE_SIGNED',
model='SurgicalNote',
object_id=str(note.note_id),
details={
'patient_name': f"{note.surgical_case.patient.first_name} {note.surgical_case.patient.last_name}",
'note_type': note.note_type
}
)
messages.success(request, 'Surgical note signed successfully.')
if request.headers.get('HX-Request'):
return render(request, 'operating_theatre/partials/note_status.html', {'note': note})
return redirect('operating_theatre:surgical_note_detail', pk=note.pk)
return JsonResponse({'success': False})
@login_required
def update_room_status(request, room_id):
"""
Update operating room status.
"""
if request.method == 'POST':
room = get_object_or_404(
OperatingRoom,
id=room_id,
tenant=request.user.tenant
)
new_status = request.POST.get('status')
if new_status in dict(OperatingRoom._meta.get_field('status').choices):
old_status = room.status
room.status = new_status
room.save()
# Log the action
AuditLogger.log_event(
user=request.user,
action='OPERATING_ROOM_STATUS_UPDATED',
model='OperatingRoom',
object_id=str(room.id),
details={
'room_number': room.room_number,
'old_status': old_status,
'new_status': new_status
}
)
messages.success(request, f'Room {room.room_number} status updated to {room.get_status_display()}.')
if request.headers.get('HX-Request'):
return render(request, 'operating_theatre/partials/room_status.html', {'room': room})
return redirect('operating_theatre:operating_room_detail', pk=room.pk)
return JsonResponse({'success': False})
@login_required
def case_search(request):
"""
HTMX endpoint for surgical case search.
"""
search = request.GET.get('search', '')
status = request.GET.get('status', '')
priority = request.GET.get('priority', '')
queryset = SurgicalCase.objects.filter(tenant=request.user.tenant)
if search:
queryset = queryset.filter(
Q(patient__first_name__icontains=search) |
Q(patient__last_name__icontains=search) |
Q(patient__mrn__icontains=search) |
Q(procedure_name__icontains=search)
)
if status:
queryset = queryset.filter(status=status)
if priority:
queryset = queryset.filter(priority=priority)
cases = queryset.select_related(
'patient', 'primary_surgeon', 'operating_room'
).order_by('-scheduled_start_time')[:20]
return render(request, 'operating_theatre/partials/case_list.html', {'cases': cases})
@login_required
def surgical_note_preview(request, pk):
"""
AJAX endpoint for surgical note preview modal.
Returns rendered preview content for modal display.
"""
surgical_note = get_object_or_404(
SurgicalNote,
id=pk,
tenant=request.user.tenant
)
context = {
'surgical_note': surgical_note,
'surgical_case': surgical_note.surgical_case,
'template': surgical_note.template_used,
'signatures': surgical_note.signatures.all().order_by('signed_at'),
'can_sign': (
surgical_note.status == 'DRAFT' and
(surgical_note.surgeon == request.user or
request.user.has_perm('operating_theatre.change_surgicalnote'))
),
'can_edit': (
surgical_note.status == 'DRAFT' and
surgical_note.surgeon == request.user
),
}
# Add equipment usage if available
if surgical_note.surgical_case:
context['equipment_usage'] = EquipmentUsage.objects.filter(
surgical_case=surgical_note.surgical_case,
tenant=request.user.tenant
).order_by('start_time')
return render(request, 'operating_theatre/partials/surgical_note_preview_modal.html', context)
def _parse_start(payload):
"""
Accepts either:
- payload['scheduled_start'] as ISO datetime string, or
- payload['date'] ('YYYY-MM-DD') + payload['start_time'] ('HH:MM' 24h)
Returns an aware datetime in the current timezone, or None.
"""
tz = timezone.get_current_timezone()
# Prefer unified datetime
if payload.get("scheduled_start"):
dt = parse_datetime(payload["scheduled_start"])
if dt is not None and timezone.is_naive(dt):
dt = timezone.make_aware(dt, tz)
return dt
# Fallback: date + time
d = parse_date(payload.get("date") or "")
t = parse_time(payload.get("start_time") or "")
if d and t:
naive = timezone.datetime.combine(d, t)
return timezone.make_aware(naive, tz)
return None
@login_required
@require_POST
def check_room_availability(request):
"""
POST JSON:
{
"room_id": <int>,
"duration": <int minutes>,
"scheduled_start": "YYYY-MM-DDTHH:MM[:SS]",
// OR:
"date": "YYYY-MM-DD",
"start_time": "HH:MM",
// Optional:
"case_id": <int>
}
"""
# ---- Parse JSON body
try:
payload = json.loads(request.body.decode("utf-8"))
except Exception:
return HttpResponseBadRequest("Invalid JSON payload")
room_id = payload.get("room_id")
duration = payload.get("duration")
case_id = payload.get("case_id")
if not room_id or duration is None:
return HttpResponseBadRequest("room_id and duration are required")
try:
duration = int(duration)
if duration <= 0:
raise ValueError
except ValueError:
return HttpResponseBadRequest("duration must be a positive integer (minutes)")
start_dt = _parse_start(payload)
if start_dt is None:
return HttpResponseBadRequest(
"Provide either 'scheduled_start' (ISO) or 'date' + 'start_time'"
)
# ---- Tenant scope & room checks
room = get_object_or_404(
OperatingRoom.objects.select_related("tenant"),
pk=room_id,
tenant=request.user.tenant,
)
# If room is not usable, return fast
if not room.is_active:
return JsonResponse({
"available": False,
"conflict_reason": "Room is inactive",
"conflicts": [],
})
if room.status in ("OUT_OF_ORDER", "MAINTENANCE", "CLOSED"):
return JsonResponse({
"available": False,
"conflict_reason": f"Room status is {room.get_status_display()}",
"conflicts": [],
})
end_dt = start_dt + timedelta(minutes=duration)
# ---- Find potentially overlapping cases
# Well fetch a reasonable window (same day +/- 1 day buffer) to reduce DB load, then check overlap in Python.
day_start = timezone.make_aware(
datetime.combine(start_dt.date(), datetime.min.time()),
timezone.get_current_timezone()
)
day_end = day_start + timedelta(days=2) # small buffer to capture cross-midnight or long cases
qs = SurgicalCase.objects.filter(
operating_room=room,
).exclude(
status__in=["CANCELLED"] # adjust based on your enum
)
if case_id:
qs = qs.exclude(pk=case_id)
# Try to use the most stable datetime field name(s) youre using across the app:
# We saw both 'scheduled_start_time' and 'scheduled_start' in your templates; handle both.
# Pull candidates that start within the window or could overlap it.
# We'll just fetch cases for the surrounding window by their scheduled start field(s).
candidates = qs.filter(
Q(scheduled_start_time__gte=day_start, scheduled_start_time__lte=day_end)
| Q(scheduled_start__gte=day_start, scheduled_start__lte=day_end)
).select_related("patient")
conflicts = []
for c in candidates:
# Determine the other case's start & duration field names robustly
other_start = getattr(c, "scheduled_start", None) or getattr(c, "scheduled_start_time", None)
if other_start is None:
continue
other_duration = (
getattr(c, "estimated_duration", None)
or getattr(c, "estimated_duration_minutes", None)
or 120 # sane default if not set
)
try:
other_duration = int(other_duration)
except Exception:
other_duration = 120
other_end = other_start + timedelta(minutes=other_duration)
# Check overlap: [start_dt, end_dt) intersects [other_start, other_end)
if (start_dt < other_end) and (end_dt > other_start):
conflicts.append({
"id": c.pk,
"patient": getattr(c.patient, "get_full_name", lambda: str(c.patient))(),
"start": timezone.localtime(other_start).strftime("%Y-%m-%d %H:%M"),
"end": timezone.localtime(other_end).strftime("%Y-%m-%d %H:%M"),
"status": getattr(c, "status", "UNKNOWN"),
})
if conflicts:
return JsonResponse({
"available": False,
"conflict_reason": "Overlaps existing case",
"conflicts": conflicts,
})
# No overlaps found
return JsonResponse({
"available": True,
"conflict_reason": "",
"conflicts": [],
})
@login_required
def operating_theatre_stats(request):
"""
HTMX endpoint for operating theatre statistics.
"""
tenant = request.user.tenant
today = timezone.now().date()
stats = {
'rooms_available': OperatingRoom.objects.filter(
tenant=tenant,
is_active=True,
status='AVAILABLE'
).count(),
'rooms_in_use': OperatingRoom.objects.filter(
tenant=tenant,
is_active=True,
status='IN_USE'
).count(),
'cases_in_progress': SurgicalCase.objects.filter(
admission__tenant=tenant,
status='IN_PROGRESS'
).count(),
'cases_completed_today': SurgicalCase.objects.filter(
admission__tenant=tenant,
actual_end__date=today,
status='COMPLETED'
).count(),
'emergency_cases_today': SurgicalCase.objects.filter(
admission__tenant=tenant,
scheduled_start__date=today,
case_type='EMERGENCY'
).count(),
}
return render(request, 'operating_theatre/partials/or_stats.html', {'stats': stats})
@login_required
def case_search(request):
"""
HTMX endpoint for surgical case search.
"""
search = request.GET.get('search', '')
status = request.GET.get('status', '')
priority = request.GET.get('priority', '')
queryset = SurgicalCase.objects.filter(tenant=request.user.tenant)
if search:
queryset = queryset.filter(
Q(patient__first_name__icontains=search) |
Q(patient__last_name__icontains=search) |
Q(patient__mrn__icontains=search) |
Q(procedure_name__icontains=search)
)
if status:
queryset = queryset.filter(status=status)
if priority:
queryset = queryset.filter(priority=priority)
cases = queryset.select_related(
'patient', 'primary_surgeon', 'operating_room'
).order_by('-scheduled_start_time')[:20]
return render(request, 'operating_theatre/partials/case_list.html', {'cases': cases})
# ============================================================================
# Surgical Case Workflow Functions
# ============================================================================
@login_required
@require_http_methods(["POST"])
def confirm_case(request, pk):
"""
Confirm a scheduled surgical case.
"""
case = get_object_or_404(
SurgicalCase,
pk=pk,
or_block__operating_room__tenant=request.user.tenant
)
# Only scheduled cases can be confirmed
if case.status != 'SCHEDULED':
messages.error(request, 'Only scheduled cases can be confirmed')
return redirect('operating_theatre:case_detail', pk=case.pk)
case.status = 'CONFIRMED'
case.save()
messages.success(request, f'Case {case.case_number} confirmed successfully')
return redirect('operating_theatre:case_detail', pk=case.pk)
@login_required
@require_http_methods(["POST"])
def prep_case(request, pk):
"""
Mark case as in pre-operative prep.
"""
case = get_object_or_404(
SurgicalCase,
pk=pk,
or_block__operating_room__tenant=request.user.tenant
)
# Only confirmed cases can move to prep
if case.status not in ['SCHEDULED', 'CONFIRMED']:
messages.error(request, 'Only scheduled or confirmed cases can move to prep')
return redirect('operating_theatre:case_detail', pk=case.pk)
case.status = 'PREP'
case.save()
messages.success(request, f'Case {case.case_number} moved to pre-operative prep')
return redirect('operating_theatre:case_detail', pk=case.pk)
@login_required
@require_http_methods(["POST"])
def cancel_case(request, pk):
"""
Cancel a surgical case.
"""
case = get_object_or_404(
SurgicalCase,
pk=pk,
or_block__operating_room__tenant=request.user.tenant
)
# Cannot cancel completed cases
if case.status == 'COMPLETED':
messages.error(request, 'Cannot cancel a completed case')
return redirect('operating_theatre:case_detail', pk=case.pk)
# Cannot cancel in-progress cases
if case.status == 'IN_PROGRESS':
messages.error(request, 'Cannot cancel a case that is in progress')
return redirect('operating_theatre:case_detail', pk=case.pk)
reason = request.POST.get('reason', '')
case.status = 'CANCELLED'
if reason:
if case.clinical_notes:
case.clinical_notes += f"\n\nCancellation Reason ({timezone.now().strftime('%Y-%m-%d %H:%M')}): {reason}"
else:
case.clinical_notes = f"Cancellation Reason: {reason}"
case.save()
messages.warning(request, f'Case {case.case_number} cancelled')
return redirect('operating_theatre:case_list')
@login_required
@require_http_methods(["POST"])
def postpone_case(request, pk):
"""
Postpone a surgical case.
"""
case = get_object_or_404(
SurgicalCase,
pk=pk,
or_block__operating_room__tenant=request.user.tenant
)
# Cannot postpone completed or in-progress cases
if case.status in ['COMPLETED', 'IN_PROGRESS']:
messages.error(request, f'Cannot postpone a {case.get_status_display().lower()} case')
return redirect('operating_theatre:case_detail', pk=case.pk)
reason = request.POST.get('reason', '')
case.status = 'POSTPONED'
if reason:
if case.clinical_notes:
case.clinical_notes += f"\n\nPostponement Reason ({timezone.now().strftime('%Y-%m-%d %H:%M')}): {reason}"
else:
case.clinical_notes = f"Postponement Reason: {reason}"
case.save()
messages.info(request, f'Case {case.case_number} postponed')
return redirect('operating_theatre:case_detail', pk=case.pk)
@login_required
def reschedule_case(request, pk):
"""
Reschedule a surgical case.
"""
case = get_object_or_404(
SurgicalCase,
pk=pk,
or_block__operating_room__tenant=request.user.tenant
)
# Cannot reschedule completed or in-progress cases
if case.status in ['COMPLETED', 'IN_PROGRESS']:
messages.error(request, f'Cannot reschedule a {case.get_status_display().lower()} case')
return redirect('operating_theatre:case_detail', pk=case.pk)
if request.method == 'POST':
# Get new OR block
or_block_id = request.POST.get('or_block')
scheduled_start = request.POST.get('scheduled_start')
if or_block_id and scheduled_start:
try:
new_or_block = ORBlock.objects.get(
pk=or_block_id,
operating_room__tenant=request.user.tenant
)
case.or_block = new_or_block
case.scheduled_start = scheduled_start
case.status = 'SCHEDULED'
# Add note about rescheduling
reschedule_note = f"\n\nRescheduled on {timezone.now().strftime('%Y-%m-%d %H:%M')} to {scheduled_start}"
if case.clinical_notes:
case.clinical_notes += reschedule_note
else:
case.clinical_notes = reschedule_note.strip()
case.save()
messages.success(request, f'Case {case.case_number} rescheduled successfully')
return redirect('operating_theatre:case_detail', pk=case.pk)
except ORBlock.DoesNotExist:
messages.error(request, 'Invalid OR block selected')
else:
messages.error(request, 'Please provide both OR block and scheduled start time')
# Get available OR blocks for rescheduling
available_blocks = ORBlock.objects.filter(
operating_room__tenant=request.user.tenant,
operating_room__is_active=True,
date__gte=timezone.now().date(),
status='SCHEDULED'
).select_related('operating_room').order_by('date', 'start_time')
context = {
'case': case,
'available_blocks': available_blocks,
}
return render(request, 'operating_theatre/case_reschedule.html', context)