""" 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.generic import ( ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView ) from django.http import JsonResponse, HttpResponse from django.db.models import Q, Count, Avg, Sum, F from django.utils import timezone 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 datetime import datetime, timedelta, date import json from core.utils import AuditLogger from .models import ( OperatingRoom, ORBlock, SurgicalCase, SurgicalNote, EquipmentUsage, SurgicalNoteTemplate ) from .forms import ( OperatingRoomForm, ORBlockForm, SurgicalCaseForm, SurgicalNoteForm, EquipmentUsageForm, SurgicalNoteTemplateForm ) # ============================================================================ # DASHBOARD AND OVERVIEW VIEWS # ============================================================================ class OperatingTheatreDashboardView(LoginRequiredMixin, TemplateView): """ Main operating theatre dashboard with key metrics and recent activity. """ 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() # Dashboard statistics 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='IN_USE' ).count(), 'rooms_maintenance': OperatingRoom.objects.filter( tenant=tenant, is_active=True, status='MAINTENANCE' ).count(), 'cases_today': SurgicalCase.objects.filter( admission__tenant=tenant, scheduled_start__date=today ).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(), 'blocks_today': ORBlock.objects.filter( operating_room__tenant=tenant, date=today ).count(), 'equipment_in_use': EquipmentUsage.objects.filter( surgical_case__admission__tenant=tenant, # status='IN_USE' ).count(), 'notes_pending': SurgicalNote.objects.filter( surgical_case__admission__tenant=tenant, status='DRAFT' ).count(), }) # Recent surgical cases context['recent_cases'] = SurgicalCase.objects.filter( admission__tenant=tenant ).select_related( 'patient', 'primary_surgeon', 'or_block__operating_room' ).order_by('-scheduled_start')[:10] # Today's schedule context['todays_schedule'] = SurgicalCase.objects.filter( admission__tenant=tenant, scheduled_start__date=today ).select_related( 'patient', 'primary_surgeon', 'or_block__operating_room' ).order_by('scheduled_start') # Room utilization # context['room_utilization'] = OperatingRoom.objects.filter( # tenant=tenant, # is_active=True # ).annotate( # cases_today=Count( # 'or_block__surgical_cases', # filter=Q(surgical_cases__scheduled_start__date=today) # ) # ).order_by('room_number') return context # ============================================================================ # OPERATING ROOM VIEWS (FULL CRUD - Master Data) # ============================================================================ 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): 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() # Get today's cases for this room context['todays_cases'] = operating_room.surgical_cases.filter( scheduled_start_time__date=today ).select_related('patient', 'primary_surgeon').order_by('scheduled_start_time') # Get recent cases context['recent_cases'] = operating_room.surgical_cases.all().select_related( 'patient', 'primary_surgeon' ).order_by('-scheduled_start_time')[:10] # Get equipment usage context['equipment_usage'] = EquipmentUsage.objects.filter( operating_room=operating_room, tenant=self.request.user.tenant ).order_by('-start_time')[:10] # Room statistics context['room_stats'] = { 'total_cases': operating_room.surgical_cases.count(), 'cases_this_month': operating_room.surgical_cases.filter( scheduled_start_time__month=timezone.now().month, scheduled_start_time__year=timezone.now().year ).count(), 'average_case_duration': operating_room.surgical_cases.filter( actual_end_time__isnull=False ).aggregate( avg_duration=Avg( F('actual_end_time') - F('actual_start_time') ) )['avg_duration'], } return context class OperatingRoomCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new operating room. """ 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 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='OPERATING_ROOM_CREATED', model='OperatingRoom', object_id=str(self.object.id), details={ 'room_number': self.object.room_number, 'room_name': self.object.room_name, 'room_type': self.object.room_type } ) 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) # ============================================================================ # SURGICAL NOTE TEMPLATE VIEWS (FULL CRUD - Master Data) # ============================================================================ 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_action( 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) # ============================================================================ # OR BLOCK VIEWS (LIMITED CRUD - Operational Data) # ============================================================================ 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 = 'or_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', '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 = 'or_block' def get_queryset(self): return ORBlock.objects.filter(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 context['scheduled_cases'] = SurgicalCase.objects.filter( operating_room=or_block.operating_room, scheduled_start_time__date=or_block.date, scheduled_start_time__time__gte=or_block.start_time, scheduled_start_time__time__lt=or_block.end_time, tenant=self.request.user.tenant ).select_related('patient', 'primary_surgeon').order_by('scheduled_start_time') # Calculate utilization total_block_minutes = ( timezone.datetime.combine(timezone.now().date(), or_block.end_time) - timezone.datetime.combine(timezone.now().date(), or_block.start_time) ).total_seconds() / 60 used_minutes = 0 for case in context['scheduled_cases']: if case.estimated_duration_minutes: used_minutes += case.estimated_duration_minutes context['utilization_percentage'] = ( (used_minutes / total_block_minutes) * 100 if total_block_minutes > 0 else 0 ) 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_action( 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_update_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 # ============================================================================ # SURGICAL CASE VIEWS (RESTRICTED CRUD - Clinical Data) # ============================================================================ class SurgicalCaseListView(LoginRequiredMixin, ListView): """ List all surgical cases with filtering and search. """ model = SurgicalCase template_name = 'operating_theatre/surgical_case_list.html' context_object_name = 'surgical_cases' paginate_by = 25 def get_queryset(self): tenant = self.request.user.tenant queryset = SurgicalCase.objects.filter(admission__tenant=tenant) # Search functionality search = self.request.GET.get('search') 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) ) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by priority priority = self.request.GET.get('priority') if priority: queryset = queryset.filter(priority=priority) # Filter by surgeon surgeon_id = self.request.GET.get('surgeon') if surgeon_id: queryset = queryset.filter(primary_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) # 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(scheduled_start__date__gte=date_from) if date_to: queryset = queryset.filter(scheduled_start__date__lte=date_to) return queryset.select_related( 'patient', 'primary_surgeon', 'or_block__operating_room' ).order_by('-scheduled_start') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'statuses': SurgicalCase.STATUS_CHOICES, 'priorities': SurgicalCase.CASE_TYPE_CHOICES, 'operating_rooms': OperatingRoom.objects.filter( tenant=self.request.user.tenant, is_active=True ).order_by('room_number'), }) 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(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.all().order_by('-created_at') # Get equipment usage for this case context['equipment_usage'] = EquipmentUsage.objects.filter( surgical_case=surgical_case, tenant=self.request.user.tenant ).order_by('-start_time') # Calculate actual duration if case is completed if surgical_case.actual_start_time and surgical_case.actual_end_time: context['actual_duration'] = surgical_case.actual_end_time - surgical_case.actual_start_time 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(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', 'notes', '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 # ============================================================================ # SURGICAL NOTE VIEWS (APPEND-ONLY - Clinical Records) # ============================================================================ class SurgicalNoteListView(LoginRequiredMixin, ListView): """ List all surgical notes with filtering and search. """ model = SurgicalNote template_name = 'operating_theatre/notes/operative_note_list.html' context_object_name = 'surgical_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' ).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.STATUS_CHOICES, }) return context class SurgicalNoteDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a surgical note. """ model = SurgicalNote template_name = 'operating_theatre/notes/operative_note_detail.html' context_object_name = 'surgical_note' def get_queryset(self): return SurgicalNote.objects.filter(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_action( 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 # Note: No UpdateView or DeleteView for SurgicalNote - Append-only for clinical records # ============================================================================ # EQUIPMENT USAGE VIEWS (LIMITED CRUD - Operational Data) # ============================================================================ 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_usage_records' 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( '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.EQUIPMENT_TYPE_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_usage_detail.html' context_object_name = 'equipment_usage' def get_queryset(self): return EquipmentUsage.objects.filter(tenant=self.request.user.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_usage_update_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_action( 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 # ============================================================================ # HTMX VIEWS FOR REAL-TIME UPDATES # ============================================================================ @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}) # ============================================================================ # ACTION VIEWS # ============================================================================ @login_required def start_case(request, case_id): """ Start a surgical case. """ if request.method == 'POST': case = get_object_or_404( SurgicalCase, id=case_id, tenant=request.user.tenant ) case.status = 'IN_PROGRESS' case.actual_start_time = timezone.now() case.save() # Update room status if case.operating_room: case.operating_room.status = 'IN_USE' case.operating_room.save() # Log the action AuditLogger.log_action( user=request.user, action='SURGICAL_CASE_STARTED', model='SurgicalCase', object_id=str(case.case_id), details={ 'patient_name': f"{case.patient.first_name} {case.patient.last_name}", 'procedure_name': case.procedure_name } ) messages.success(request, 'Surgical case started successfully.') if request.headers.get('HX-Request'): return render(request, 'operating_theatre/partials/case_status.html', {'case': case}) return redirect('operating_theatre:surgical_case_detail', pk=case.pk) return JsonResponse({'success': False}) @login_required def complete_case(request, case_id): """ Complete a surgical case. """ if request.method == 'POST': case = get_object_or_404( SurgicalCase, id=case_id, tenant=request.user.tenant ) case.status = 'COMPLETED' case.actual_end_time = timezone.now() case.save() # Update room status if case.operating_room: case.operating_room.status = 'CLEANING' case.operating_room.save() # Log the action AuditLogger.log_action( user=request.user, action='SURGICAL_CASE_COMPLETED', model='SurgicalCase', object_id=str(case.case_id), details={ 'patient_name': f"{case.patient.first_name} {case.patient.last_name}", 'procedure_name': case.procedure_name, 'duration': str(case.actual_end_time - case.actual_start_time) if case.actual_start_time else None } ) messages.success(request, 'Surgical case completed successfully.') if request.headers.get('HX-Request'): return render(request, 'operating_theatre/partials/case_status.html', {'case': case}) return redirect('operating_theatre:surgical_case_detail', pk=case.pk) return JsonResponse({'success': False}) @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_action( 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_action( 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}) # # """ # 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.generic import ( # ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView # ) # from django.http import JsonResponse, HttpResponse # from django.db.models import Q, Count, Avg, Sum, F # from django.utils import timezone # 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 datetime import datetime, timedelta, date # import json # # from core.utils import AuditLogger # from .models import ( # OperatingRoom, ORBlock, SurgicalCase, SurgicalNote, # EquipmentUsage, SurgicalNoteTemplate # ) # from .forms import ( # OperatingRoomForm, ORBlockForm, SurgicalCaseForm, SurgicalNoteForm, # EquipmentUsageForm, SurgicalNoteTemplateForm # ) # # # # ============================================================================ # # DASHBOARD AND OVERVIEW VIEWS # # ============================================================================ # # class OperatingTheatreDashboardView(LoginRequiredMixin, TemplateView): # """ # Main operating theatre dashboard with key metrics and recent activity. # """ # 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() # # # Dashboard statistics # 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='IN_USE' # ).count(), # 'rooms_maintenance': OperatingRoom.objects.filter( # tenant=tenant, # is_active=True, # status='MAINTENANCE' # ).count(), # 'cases_today': SurgicalCase.objects.filter( # tenant=tenant, # scheduled_start_time__date=today # ).count(), # 'cases_in_progress': SurgicalCase.objects.filter( # tenant=tenant, # status='IN_PROGRESS' # ).count(), # 'cases_completed_today': SurgicalCase.objects.filter( # tenant=tenant, # actual_end_time__date=today, # status='COMPLETED' # ).count(), # 'emergency_cases_today': SurgicalCase.objects.filter( # tenant=tenant, # scheduled_start_time__date=today, # priority='EMERGENCY' # ).count(), # 'blocks_today': ORBlock.objects.filter( # tenant=tenant, # date=today # ).count(), # 'equipment_in_use': EquipmentUsage.objects.filter( # tenant=tenant, # status='IN_USE' # ).count(), # 'notes_pending': SurgicalNote.objects.filter( # tenant=tenant, # status='DRAFT' # ).count(), # }) # # # Recent surgical cases # context['recent_cases'] = SurgicalCase.objects.filter( # tenant=tenant # ).select_related( # 'patient', 'primary_surgeon', 'operating_room' # ).order_by('-scheduled_start_time')[:10] # # # Today's schedule # context['todays_schedule'] = SurgicalCase.objects.filter( # tenant=tenant, # scheduled_start_time__date=today # ).select_related( # 'patient', 'primary_surgeon', 'operating_room' # ).order_by('scheduled_start_time') # # # Room utilization # context['room_utilization'] = OperatingRoom.objects.filter( # tenant=tenant, # is_active=True # ).annotate( # cases_today=Count( # 'surgical_cases', # filter=Q(surgical_cases__scheduled_start_time__date=today) # ) # ).order_by('room_number') # # return context # # # # ============================================================================ # # OPERATING ROOM VIEWS (FULL CRUD - Master Data) # # ============================================================================ # # class OperatingRoomListView(LoginRequiredMixin, ListView): # """ # List all operating rooms with filtering and search. # """ # model = OperatingRoom # template_name = 'operating_theatre/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/operating_room_detail.html' # context_object_name = 'operating_room' # # def get_queryset(self): # 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() # # # Get today's cases for this room # context['todays_cases'] = operating_room.surgical_cases.filter( # scheduled_start_time__date=today # ).select_related('patient', 'primary_surgeon').order_by('scheduled_start_time') # # # Get recent cases # context['recent_cases'] = operating_room.surgical_cases.all().select_related( # 'patient', 'primary_surgeon' # ).order_by('-scheduled_start_time')[:10] # # # Get equipment usage # context['equipment_usage'] = EquipmentUsage.objects.filter( # operating_room=operating_room, # tenant=self.request.user.tenant # ).order_by('-start_time')[:10] # # # Room statistics # context['room_stats'] = { # 'total_cases': operating_room.surgical_cases.count(), # 'cases_this_month': operating_room.surgical_cases.filter( # scheduled_start_time__month=timezone.now().month, # scheduled_start_time__year=timezone.now().year # ).count(), # 'average_case_duration': operating_room.surgical_cases.filter( # actual_end_time__isnull=False # ).aggregate( # avg_duration=Avg( # F('actual_end_time') - F('actual_start_time') # ) # )['avg_duration'], # } # # return context # # # class OperatingRoomCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new operating room. # """ # model = OperatingRoom # form_class = OperatingRoomForm # template_name = 'operating_theatre/operating_room_form.html' # permission_required = 'operating_theatre.add_operatingroom' # success_url = reverse_lazy('operating_theatre:operating_room_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='OPERATING_ROOM_CREATED', # model='OperatingRoom', # object_id=str(self.object.id), # details={ # 'room_number': self.object.room_number, # 'room_name': self.object.room_name, # 'room_type': self.object.room_type # } # ) # # 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/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/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_action( # 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) # # # # ============================================================================ # # SURGICAL NOTE TEMPLATE VIEWS (FULL CRUD - Master Data) # # ============================================================================ # # class SurgicalNoteTemplateListView(LoginRequiredMixin, ListView): # """ # List all surgical note templates with filtering and search. # """ # model = SurgicalNoteTemplate # template_name = 'operating_theatre/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('template_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/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/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_action( # 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/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_action( # 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/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_action( # 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) # # # # ============================================================================ # # OR BLOCK VIEWS (LIMITED CRUD - Operational Data) # # ============================================================================ # # class ORBlockListView(LoginRequiredMixin, ListView): # """ # List all OR blocks with filtering and search. # """ # model = ORBlock # template_name = 'operating_theatre/or_block_list.html' # context_object_name = 'or_blocks' # paginate_by = 25 # # def get_queryset(self): # queryset = ORBlock.objects.filter(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', '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/or_block_detail.html' # context_object_name = 'or_block' # # def get_queryset(self): # return ORBlock.objects.filter(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 # context['scheduled_cases'] = SurgicalCase.objects.filter( # operating_room=or_block.operating_room, # scheduled_start_time__date=or_block.date, # scheduled_start_time__time__gte=or_block.start_time, # scheduled_start_time__time__lt=or_block.end_time, # tenant=self.request.user.tenant # ).select_related('patient', 'primary_surgeon').order_by('scheduled_start_time') # # # Calculate utilization # total_block_minutes = ( # timezone.datetime.combine(timezone.now().date(), or_block.end_time) - # timezone.datetime.combine(timezone.now().date(), or_block.start_time) # ).total_seconds() / 60 # # used_minutes = 0 # for case in context['scheduled_cases']: # if case.estimated_duration_minutes: # used_minutes += case.estimated_duration_minutes # # context['utilization_percentage'] = ( # (used_minutes / total_block_minutes) * 100 if total_block_minutes > 0 else 0 # ) # # return context # # # class ORBlockCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new OR block. # """ # model = ORBlock # form_class = ORBlockForm # template_name = 'operating_theatre/or_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_action( # 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_update_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 # # # # ============================================================================ # # SURGICAL CASE VIEWS (RESTRICTED CRUD - Clinical Data) # # ============================================================================ # # class SurgicalCaseListView(LoginRequiredMixin, ListView): # """ # List all surgical cases with filtering and search. # """ # model = SurgicalCase # template_name = 'operating_theatre/surgical_case_list.html' # context_object_name = 'surgical_cases' # paginate_by = 25 # # def get_queryset(self): # queryset = SurgicalCase.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # 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) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by priority # priority = self.request.GET.get('priority') # if priority: # queryset = queryset.filter(priority=priority) # # # Filter by surgeon # surgeon_id = self.request.GET.get('surgeon') # if surgeon_id: # queryset = queryset.filter(primary_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) # # # 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(scheduled_start_time__date__gte=date_from) # if date_to: # queryset = queryset.filter(scheduled_start_time__date__lte=date_to) # # return queryset.select_related( # 'patient', 'primary_surgeon', 'operating_room' # ).order_by('-scheduled_start_time') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'statuses': SurgicalCase._meta.get_field('status').choices, # 'priorities': SurgicalCase._meta.get_field('priority').choices, # 'operating_rooms': OperatingRoom.objects.filter( # tenant=self.request.user.tenant, # is_active=True # ).order_by('room_number'), # }) # return context # # # class SurgicalCaseDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a surgical case. # """ # model = SurgicalCase # template_name = 'operating_theatre/surgical_case_detail.html' # context_object_name = 'surgical_case' # # def get_queryset(self): # return SurgicalCase.objects.filter(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.all().order_by('-created_at') # # # Get equipment usage for this case # context['equipment_usage'] = EquipmentUsage.objects.filter( # surgical_case=surgical_case, # tenant=self.request.user.tenant # ).order_by('-start_time') # # # Calculate actual duration if case is completed # if surgical_case.actual_start_time and surgical_case.actual_end_time: # context['actual_duration'] = surgical_case.actual_end_time - surgical_case.actual_start_time # # return context # # # class SurgicalCaseCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new surgical case. # """ # model = SurgicalCase # form_class = SurgicalCaseForm # template_name = 'operating_theatre/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/surgical_case_update_form.html' # permission_required = 'operating_theatre.change_surgicalcase' # # def get_queryset(self): # return SurgicalCase.objects.filter(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', 'notes', '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_action( # 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 # # # # ============================================================================ # # SURGICAL NOTE VIEWS (APPEND-ONLY - Clinical Records) # # ============================================================================ # # class SurgicalNoteListView(LoginRequiredMixin, ListView): # """ # List all surgical notes with filtering and search. # """ # model = SurgicalNote # template_name = 'operating_theatre/surgical_note_list.html' # context_object_name = 'surgical_notes' # paginate_by = 25 # # def get_queryset(self): # queryset = SurgicalNote.objects.filter(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' # ).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._meta.get_field('status').choices, # }) # return context # # # class SurgicalNoteDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a surgical note. # """ # model = SurgicalNote # template_name = 'operating_theatre/surgical_note_detail.html' # context_object_name = 'surgical_note' # # def get_queryset(self): # return SurgicalNote.objects.filter(tenant=self.request.user.tenant) # # # class SurgicalNoteCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new surgical note. # """ # model = SurgicalNote # form_class = SurgicalNoteForm # template_name = 'operating_theatre/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_action( # 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 # # # # Note: No UpdateView or DeleteView for SurgicalNote - Append-only for clinical records # # # # ============================================================================ # # EQUIPMENT USAGE VIEWS (LIMITED CRUD - Operational Data) # # ============================================================================ # # class EquipmentUsageListView(LoginRequiredMixin, ListView): # """ # List all equipment usage records with filtering and search. # """ # model = EquipmentUsage # template_name = 'operating_theatre/equipment_usage_list.html' # context_object_name = 'equipment_usage_records' # paginate_by = 25 # # def get_queryset(self): # queryset = EquipmentUsage.objects.filter(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( # '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._meta.get_field('equipment_type').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_usage_detail.html' # context_object_name = 'equipment_usage' # # def get_queryset(self): # return EquipmentUsage.objects.filter(tenant=self.request.user.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_usage_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_usage_update_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_action( # 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 # # # # ============================================================================ # # HTMX VIEWS FOR REAL-TIME UPDATES # # ============================================================================ # # @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( # tenant=tenant, # status='IN_PROGRESS' # ).count(), # 'cases_completed_today': SurgicalCase.objects.filter( # tenant=tenant, # actual_end_time__date=today, # status='COMPLETED' # ).count(), # 'emergency_cases_today': SurgicalCase.objects.filter( # tenant=tenant, # scheduled_start_time__date=today, # priority='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}) # # # # ============================================================================ # # ACTION VIEWS # # ============================================================================ # # @login_required # def start_case(request, case_id): # """ # Start a surgical case. # """ # if request.method == 'POST': # case = get_object_or_404( # SurgicalCase, # id=case_id, # tenant=request.user.tenant # ) # # case.status = 'IN_PROGRESS' # case.actual_start_time = timezone.now() # case.save() # # # Update room status # if case.operating_room: # case.operating_room.status = 'IN_USE' # case.operating_room.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='SURGICAL_CASE_STARTED', # model='SurgicalCase', # object_id=str(case.case_id), # details={ # 'patient_name': f"{case.patient.first_name} {case.patient.last_name}", # 'procedure_name': case.procedure_name # } # ) # # messages.success(request, 'Surgical case started successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'operating_theatre/partials/case_status.html', {'case': case}) # # return redirect('operating_theatre:surgical_case_detail', pk=case.pk) # # return JsonResponse({'success': False}) # # # @login_required # def complete_case(request, case_id): # """ # Complete a surgical case. # """ # if request.method == 'POST': # case = get_object_or_404( # SurgicalCase, # id=case_id, # tenant=request.user.tenant # ) # # case.status = 'COMPLETED' # case.actual_end_time = timezone.now() # case.save() # # # Update room status # if case.operating_room: # case.operating_room.status = 'CLEANING' # case.operating_room.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='SURGICAL_CASE_COMPLETED', # model='SurgicalCase', # object_id=str(case.case_id), # details={ # 'patient_name': f"{case.patient.first_name} {case.patient.last_name}", # 'procedure_name': case.procedure_name, # 'duration': str(case.actual_end_time - case.actual_start_time) if case.actual_start_time else None # } # ) # # messages.success(request, 'Surgical case completed successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'operating_theatre/partials/case_status.html', {'case': case}) # # return redirect('operating_theatre:surgical_case_detail', pk=case.pk) # # return JsonResponse({'success': False}) # # # @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_action( # 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_action( # 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}) # # # #