599 lines
24 KiB
Python
599 lines
24 KiB
Python
from rest_framework import viewsets, permissions, status
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
from rest_framework import filters
|
|
from django.db.models import Q, Count, Avg, Sum
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
|
|
from ..models import (
|
|
OperatingRoom, ORBlock, SurgicalCase, SurgicalNote,
|
|
EquipmentUsage, SurgicalNoteTemplate
|
|
)
|
|
from .serializers import (
|
|
OperatingRoomSerializer, ORBlockSerializer, SurgicalCaseSerializer,
|
|
SurgicalNoteSerializer, EquipmentUsageSerializer, SurgicalNoteTemplateSerializer,
|
|
ORStatsSerializer, CaseSchedulingSerializer, CaseStartSerializer,
|
|
CaseCompletionSerializer, SurgicalNoteDictationSerializer, EquipmentTrackingSerializer
|
|
)
|
|
from core.utils import AuditLogger
|
|
|
|
|
|
class BaseViewSet(viewsets.ModelViewSet):
|
|
"""Base ViewSet with common functionality"""
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
|
|
|
def get_queryset(self):
|
|
# Filter by tenant if user has one
|
|
if hasattr(self.request.user, 'tenant') and self.request.user.tenant:
|
|
return self.queryset.filter(tenant=self.request.user.tenant)
|
|
return self.queryset
|
|
|
|
def perform_create(self, serializer):
|
|
if hasattr(self.request.user, 'tenant'):
|
|
serializer.save(tenant=self.request.user.tenant)
|
|
else:
|
|
serializer.save()
|
|
|
|
|
|
class OperatingRoomViewSet(BaseViewSet):
|
|
"""ViewSet for OperatingRoom model"""
|
|
queryset = OperatingRoom.objects.all()
|
|
serializer_class = OperatingRoomSerializer
|
|
filterset_fields = ['room_type', 'status', 'floor', 'building', 'is_active']
|
|
search_fields = ['room_number', 'name', 'equipment_list', 'capabilities']
|
|
ordering_fields = ['room_number', 'name', 'capacity']
|
|
ordering = ['room_number']
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def available(self, request):
|
|
"""Get available operating rooms"""
|
|
queryset = self.get_queryset().filter(status='AVAILABLE', is_active=True)
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def maintenance_due(self, request):
|
|
"""Get rooms due for maintenance"""
|
|
today = timezone.now().date()
|
|
queryset = self.get_queryset().filter(
|
|
next_maintenance__lte=today,
|
|
is_active=True
|
|
)
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def set_status(self, request, pk=None):
|
|
"""Set operating room status"""
|
|
room = self.get_object()
|
|
new_status = request.data.get('status')
|
|
reason = request.data.get('reason', '')
|
|
|
|
if not new_status:
|
|
return Response(
|
|
{'error': 'Status is required'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
old_status = room.status
|
|
room.status = new_status
|
|
room.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='ROOM_STATUS_CHANGED',
|
|
model='OperatingRoom',
|
|
object_id=str(room.room_id),
|
|
details={
|
|
'room_number': room.room_number,
|
|
'old_status': old_status,
|
|
'new_status': new_status,
|
|
'reason': reason
|
|
}
|
|
)
|
|
|
|
return Response({'message': 'Room status updated successfully'})
|
|
|
|
|
|
class ORBlockViewSet(BaseViewSet):
|
|
"""ViewSet for ORBlock model"""
|
|
queryset = ORBlock.objects.all()
|
|
serializer_class = ORBlockSerializer
|
|
filterset_fields = [
|
|
'operating_room', 'primary_surgeon', 'date', 'block_type',
|
|
'specialty', 'is_emergency_block'
|
|
]
|
|
search_fields = ['primary_surgeon__first_name', 'primary_surgeon__last_name', 'specialty']
|
|
ordering_fields = ['date', 'start_time']
|
|
ordering = ['date', 'start_time']
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def today(self, request):
|
|
"""Get today's OR blocks"""
|
|
today = timezone.now().date()
|
|
queryset = self.get_queryset().filter(date=today)
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def by_surgeon(self, request):
|
|
"""Get blocks by surgeon"""
|
|
surgeon_id = request.query_params.get('surgeon_id')
|
|
if surgeon_id:
|
|
queryset = self.get_queryset().filter(primary_surgeon_id=surgeon_id)
|
|
else:
|
|
queryset = self.get_queryset()
|
|
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
|
|
class SurgicalCaseViewSet(BaseViewSet):
|
|
"""ViewSet for SurgicalCase model"""
|
|
queryset = SurgicalCase.objects.all()
|
|
serializer_class = SurgicalCaseSerializer
|
|
filterset_fields = [
|
|
'status', 'priority', 'case_type', 'patient', 'primary_surgeon',
|
|
'operating_room', 'scheduled_date'
|
|
]
|
|
search_fields = [
|
|
'case_number', 'patient__first_name', 'patient__last_name',
|
|
'patient__mrn', 'procedure_name', 'primary_surgeon__first_name',
|
|
'primary_surgeon__last_name'
|
|
]
|
|
ordering_fields = ['scheduled_date', 'scheduled_start_time', 'priority']
|
|
ordering = ['scheduled_date', 'scheduled_start_time']
|
|
|
|
@action(detail=False, methods=['post'])
|
|
def schedule(self, request):
|
|
"""Schedule a surgical case"""
|
|
serializer = CaseSchedulingSerializer(data=request.data)
|
|
|
|
if serializer.is_valid():
|
|
# Create surgical case
|
|
case = SurgicalCase.objects.create(
|
|
patient_id=serializer.validated_data['patient_id'],
|
|
primary_surgeon_id=serializer.validated_data['primary_surgeon_id'],
|
|
operating_room_id=serializer.validated_data['operating_room_id'],
|
|
scheduled_date=serializer.validated_data['scheduled_date'],
|
|
scheduled_start_time=serializer.validated_data['scheduled_start_time'],
|
|
procedure_name=serializer.validated_data['procedure_name'],
|
|
procedure_codes=serializer.validated_data['procedure_codes'],
|
|
case_type=serializer.validated_data['case_type'],
|
|
priority=serializer.validated_data['priority'],
|
|
anesthesia_type=serializer.validated_data['anesthesia_type'],
|
|
special_equipment=serializer.validated_data.get('special_equipment', ''),
|
|
notes=serializer.validated_data.get('notes', ''),
|
|
status='SCHEDULED',
|
|
tenant=getattr(request.user, 'tenant', None)
|
|
)
|
|
|
|
# Calculate estimated end time
|
|
estimated_duration = serializer.validated_data['estimated_duration']
|
|
start_datetime = timezone.datetime.combine(
|
|
case.scheduled_date, case.scheduled_start_time
|
|
)
|
|
end_datetime = start_datetime + timedelta(minutes=estimated_duration)
|
|
case.scheduled_end_time = end_datetime.time()
|
|
case.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='CASE_SCHEDULED',
|
|
model='SurgicalCase',
|
|
object_id=str(case.case_id),
|
|
details={
|
|
'case_number': case.case_number,
|
|
'procedure': case.procedure_name,
|
|
'scheduled_date': str(case.scheduled_date)
|
|
}
|
|
)
|
|
|
|
return Response({
|
|
'message': 'Case scheduled successfully',
|
|
'case': SurgicalCaseSerializer(case).data
|
|
})
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def today(self, request):
|
|
"""Get today's surgical cases"""
|
|
today = timezone.now().date()
|
|
queryset = self.get_queryset().filter(scheduled_date=today)
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def emergency(self, request):
|
|
"""Get emergency cases"""
|
|
queryset = self.get_queryset().filter(priority='EMERGENCY')
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def start(self, request, pk=None):
|
|
"""Start a surgical case"""
|
|
case = self.get_object()
|
|
serializer = CaseStartSerializer(data=request.data)
|
|
|
|
if serializer.is_valid():
|
|
case.status = 'IN_PROGRESS'
|
|
case.actual_start_time = timezone.now()
|
|
|
|
# Update staff assignments
|
|
if serializer.validated_data.get('anesthesiologist_id'):
|
|
case.anesthesiologist_id = serializer.validated_data['anesthesiologist_id']
|
|
if serializer.validated_data.get('scrub_nurse_id'):
|
|
case.scrub_nurse_id = serializer.validated_data['scrub_nurse_id']
|
|
if serializer.validated_data.get('circulating_nurse_id'):
|
|
case.circulating_nurse_id = serializer.validated_data['circulating_nurse_id']
|
|
|
|
case.position = serializer.validated_data.get('position', '')
|
|
case.notes = serializer.validated_data.get('notes', '')
|
|
case.save()
|
|
|
|
# Update room status
|
|
case.operating_room.status = 'OCCUPIED'
|
|
case.operating_room.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='CASE_STARTED',
|
|
model='SurgicalCase',
|
|
object_id=str(case.case_id),
|
|
details={
|
|
'case_number': case.case_number,
|
|
'procedure': case.procedure_name
|
|
}
|
|
)
|
|
|
|
return Response({'message': 'Case started successfully'})
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def complete(self, request, pk=None):
|
|
"""Complete a surgical case"""
|
|
case = self.get_object()
|
|
serializer = CaseCompletionSerializer(data=request.data)
|
|
|
|
if serializer.is_valid():
|
|
case.status = 'COMPLETED'
|
|
case.actual_end_time = timezone.now()
|
|
case.estimated_blood_loss = serializer.validated_data.get('estimated_blood_loss')
|
|
case.complications = serializer.validated_data.get('complications', '')
|
|
case.postop_destination = serializer.validated_data.get('postop_destination', '')
|
|
case.notes = serializer.validated_data.get('notes', '')
|
|
case.save()
|
|
|
|
# Update room status
|
|
case.operating_room.status = 'CLEANING'
|
|
case.operating_room.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='CASE_COMPLETED',
|
|
model='SurgicalCase',
|
|
object_id=str(case.case_id),
|
|
details={
|
|
'case_number': case.case_number,
|
|
'procedure': case.procedure_name,
|
|
'duration_minutes': case.duration_minutes
|
|
}
|
|
)
|
|
|
|
return Response({'message': 'Case completed successfully'})
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def cancel(self, request, pk=None):
|
|
"""Cancel a surgical case"""
|
|
case = self.get_object()
|
|
reason = request.data.get('reason', '')
|
|
|
|
case.status = 'CANCELLED'
|
|
case.notes = f"Cancelled: {reason}"
|
|
case.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='CASE_CANCELLED',
|
|
model='SurgicalCase',
|
|
object_id=str(case.case_id),
|
|
details={
|
|
'case_number': case.case_number,
|
|
'reason': reason
|
|
}
|
|
)
|
|
|
|
return Response({'message': 'Case cancelled successfully'})
|
|
|
|
|
|
class SurgicalNoteTemplateViewSet(BaseViewSet):
|
|
"""ViewSet for SurgicalNoteTemplate model"""
|
|
queryset = SurgicalNoteTemplate.objects.all()
|
|
serializer_class = SurgicalNoteTemplateSerializer
|
|
filterset_fields = ['procedure_type', 'specialty', 'is_active']
|
|
search_fields = ['name', 'description', 'template_content']
|
|
ordering_fields = ['name', 'procedure_type']
|
|
ordering = ['name']
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def by_procedure(self, request):
|
|
"""Get templates by procedure type"""
|
|
procedure_type = request.query_params.get('procedure_type')
|
|
if procedure_type:
|
|
queryset = self.get_queryset().filter(
|
|
procedure_type=procedure_type,
|
|
is_active=True
|
|
)
|
|
else:
|
|
queryset = self.get_queryset().filter(is_active=True)
|
|
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
|
|
class SurgicalNoteViewSet(BaseViewSet):
|
|
"""ViewSet for SurgicalNote model"""
|
|
queryset = SurgicalNote.objects.all()
|
|
serializer_class = SurgicalNoteSerializer
|
|
filterset_fields = [
|
|
'surgical_case', 'surgeon', 'note_type', 'is_signed'
|
|
]
|
|
search_fields = [
|
|
'surgical_case__case_number', 'preoperative_diagnosis',
|
|
'postoperative_diagnosis', 'procedure_performed'
|
|
]
|
|
ordering_fields = ['dictated_date', 'signed_date']
|
|
ordering = ['-dictated_date']
|
|
|
|
@action(detail=False, methods=['post'])
|
|
def dictate(self, request):
|
|
"""Dictate a surgical note"""
|
|
serializer = SurgicalNoteDictationSerializer(data=request.data)
|
|
|
|
if serializer.is_valid():
|
|
case = SurgicalCase.objects.get(id=serializer.validated_data['case_id'])
|
|
template_id = serializer.validated_data.get('template_id')
|
|
template = None
|
|
if template_id:
|
|
template = SurgicalNoteTemplate.objects.get(id=template_id)
|
|
|
|
# Create surgical note
|
|
note = SurgicalNote.objects.create(
|
|
surgical_case=case,
|
|
template=template,
|
|
surgeon=request.user,
|
|
note_type=serializer.validated_data['note_type'],
|
|
preoperative_diagnosis=serializer.validated_data['preoperative_diagnosis'],
|
|
postoperative_diagnosis=serializer.validated_data['postoperative_diagnosis'],
|
|
procedure_performed=serializer.validated_data['procedure_performed'],
|
|
indications=serializer.validated_data['indications'],
|
|
findings=serializer.validated_data['findings'],
|
|
technique=serializer.validated_data['technique'],
|
|
complications=serializer.validated_data.get('complications', ''),
|
|
estimated_blood_loss=serializer.validated_data.get('estimated_blood_loss'),
|
|
specimens=serializer.validated_data.get('specimens', ''),
|
|
implants=serializer.validated_data.get('implants', ''),
|
|
drains=serializer.validated_data.get('drains', ''),
|
|
closure=serializer.validated_data['closure'],
|
|
postoperative_plan=serializer.validated_data['postoperative_plan'],
|
|
dictated_date=timezone.now(),
|
|
tenant=getattr(request.user, 'tenant', None)
|
|
)
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='SURGICAL_NOTE_DICTATED',
|
|
model='SurgicalNote',
|
|
object_id=str(note.note_id),
|
|
details={
|
|
'case_number': case.case_number,
|
|
'note_type': note.note_type
|
|
}
|
|
)
|
|
|
|
return Response({
|
|
'message': 'Surgical note dictated successfully',
|
|
'note': SurgicalNoteSerializer(note).data
|
|
})
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def pending_signature(self, request):
|
|
"""Get notes pending signature"""
|
|
queryset = self.get_queryset().filter(is_signed=False)
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def sign(self, request, pk=None):
|
|
"""Sign a surgical note"""
|
|
note = self.get_object()
|
|
|
|
note.is_signed = True
|
|
note.signed_date = timezone.now()
|
|
note.electronic_signature = f"Electronically signed by {request.user.get_full_name()}"
|
|
note.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='SURGICAL_NOTE_SIGNED',
|
|
model='SurgicalNote',
|
|
object_id=str(note.note_id),
|
|
details={
|
|
'case_number': note.surgical_case.case_number,
|
|
'note_type': note.note_type
|
|
}
|
|
)
|
|
|
|
return Response({'message': 'Surgical note signed successfully'})
|
|
|
|
|
|
class EquipmentUsageViewSet(BaseViewSet):
|
|
"""ViewSet for EquipmentUsage model"""
|
|
queryset = EquipmentUsage.objects.all()
|
|
serializer_class = EquipmentUsageSerializer
|
|
filterset_fields = [
|
|
'surgical_case', 'equipment_name', 'used_by', 'status',
|
|
'maintenance_required'
|
|
]
|
|
search_fields = ['equipment_name', 'equipment_id', 'serial_number']
|
|
ordering_fields = ['start_time', 'end_time']
|
|
ordering = ['-start_time']
|
|
|
|
@action(detail=False, methods=['post'])
|
|
def track_usage(self, request):
|
|
"""Track equipment usage"""
|
|
serializer = EquipmentTrackingSerializer(data=request.data)
|
|
|
|
if serializer.is_valid():
|
|
case = SurgicalCase.objects.get(id=serializer.validated_data['case_id'])
|
|
|
|
# Create equipment usage record
|
|
usage = EquipmentUsage.objects.create(
|
|
surgical_case=case,
|
|
equipment_name=serializer.validated_data['equipment_name'],
|
|
equipment_id=serializer.validated_data['equipment_id'],
|
|
serial_number=serializer.validated_data.get('serial_number', ''),
|
|
start_time=serializer.validated_data['start_time'],
|
|
end_time=serializer.validated_data.get('end_time'),
|
|
used_by=request.user,
|
|
maintenance_required=serializer.validated_data.get('maintenance_required', False),
|
|
issues_reported=serializer.validated_data.get('issues_reported', ''),
|
|
notes=serializer.validated_data.get('notes', ''),
|
|
tenant=getattr(request.user, 'tenant', None)
|
|
)
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='EQUIPMENT_USAGE_TRACKED',
|
|
model='EquipmentUsage',
|
|
object_id=str(usage.usage_id),
|
|
details={
|
|
'equipment_name': usage.equipment_name,
|
|
'case_number': case.case_number
|
|
}
|
|
)
|
|
|
|
return Response({
|
|
'message': 'Equipment usage tracked successfully',
|
|
'usage': EquipmentUsageSerializer(usage).data
|
|
})
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def maintenance_required(self, request):
|
|
"""Get equipment requiring maintenance"""
|
|
queryset = self.get_queryset().filter(maintenance_required=True)
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
|
|
class ORStatsViewSet(viewsets.ViewSet):
|
|
"""ViewSet for operating room statistics"""
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def dashboard(self, request):
|
|
"""Get OR dashboard statistics"""
|
|
tenant_filter = {}
|
|
if hasattr(request.user, 'tenant') and request.user.tenant:
|
|
tenant_filter['tenant'] = request.user.tenant
|
|
|
|
today = timezone.now().date()
|
|
|
|
# Case statistics
|
|
cases = SurgicalCase.objects.filter(**tenant_filter)
|
|
total_cases = cases.count()
|
|
cases_today = cases.filter(scheduled_date=today).count()
|
|
emergency_cases = cases.filter(
|
|
priority='EMERGENCY',
|
|
scheduled_date=today
|
|
).count()
|
|
completed_cases = cases.filter(
|
|
status='COMPLETED',
|
|
scheduled_date=today
|
|
).count()
|
|
cancelled_cases = cases.filter(
|
|
status='CANCELLED',
|
|
scheduled_date=today
|
|
).count()
|
|
|
|
# Average duration
|
|
completed_today = cases.filter(
|
|
status='COMPLETED',
|
|
actual_end_time__date=today
|
|
)
|
|
durations = [case.duration_minutes for case in completed_today if case.duration_minutes]
|
|
average_duration = sum(durations) / len(durations) if durations else 0
|
|
|
|
# Room utilization (mock data for now)
|
|
rooms = OperatingRoom.objects.filter(**tenant_filter, is_active=True)
|
|
room_utilization = {}
|
|
for room in rooms:
|
|
# Calculate utilization based on scheduled cases
|
|
room_cases = cases.filter(
|
|
operating_room=room,
|
|
scheduled_date=today,
|
|
status__in=['SCHEDULED', 'IN_PROGRESS', 'COMPLETED']
|
|
)
|
|
total_minutes = sum([
|
|
case.duration_minutes or 120 # default 2 hours
|
|
for case in room_cases
|
|
])
|
|
# Assume 8-hour working day (480 minutes)
|
|
utilization = min((total_minutes / 480) * 100, 100)
|
|
room_utilization[room.room_number] = round(utilization, 1)
|
|
|
|
# Surgeon productivity (top 5)
|
|
surgeon_productivity = list(
|
|
cases.filter(scheduled_date=today, status='COMPLETED')
|
|
.values('primary_surgeon__first_name', 'primary_surgeon__last_name')
|
|
.annotate(cases_count=Count('id'))
|
|
.order_by('-cases_count')[:5]
|
|
)
|
|
|
|
# Case types breakdown
|
|
case_types = cases.filter(scheduled_date=today).values('case_type').annotate(
|
|
count=Count('id')
|
|
).order_by('-count')
|
|
|
|
# Status breakdown
|
|
status_breakdown = cases.filter(scheduled_date=today).values('status').annotate(
|
|
count=Count('id')
|
|
).order_by('-count')
|
|
|
|
stats = {
|
|
'total_cases': total_cases,
|
|
'cases_today': cases_today,
|
|
'emergency_cases': emergency_cases,
|
|
'completed_cases': completed_cases,
|
|
'cancelled_cases': cancelled_cases,
|
|
'average_duration': round(average_duration, 1),
|
|
'room_utilization': room_utilization,
|
|
'surgeon_productivity': surgeon_productivity,
|
|
'case_types': {item['case_type']: item['count'] for item in case_types},
|
|
'status_breakdown': {item['status']: item['count'] for item in status_breakdown}
|
|
}
|
|
|
|
serializer = ORStatsSerializer(stats)
|
|
return Response(serializer.data)
|
|
|