""" 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.views.decorators.http import require_POST 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 django.http import JsonResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponse from datetime import datetime, timedelta, date import json from django.utils import timezone from django.utils.dateparse import parse_datetime, parse_date, parse_time 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): 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] ) # --- Today’s 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 # ============================================================================ # 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): # 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) # ============================================================================ # 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_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) # ============================================================================ # 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 = '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 # ============================================================================ # 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/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.STATUS_CHOICES, 'case_types': SurgicalCase.CASE_TYPE_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 # ============================================================================ # 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/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.STATUS_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 # 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/equipment_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/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 # ============================================================================ # 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 # ============================================================================ 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') # @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_event( 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}) # # """ # 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}) # # # # 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 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": , "duration": , "scheduled_start": "YYYY-MM-DDTHH:MM[:SS]", // OR: "date": "YYYY-MM-DD", "start_time": "HH:MM", // Optional: "case_id": } """ # ---- 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 # We’ll 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) you’re 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": [], })