534 lines
20 KiB
Python
534 lines
20 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, F
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
|
|
from ..models import LabTest, LabOrder, Specimen, LabResult, QualityControl, ReferenceRange
|
|
from .serializers import (
|
|
LabTestSerializer, LabOrderSerializer, SpecimenSerializer, LabResultSerializer,
|
|
QualityControlSerializer, ReferenceRangeSerializer, LabStatsSerializer,
|
|
SpecimenCollectionSerializer, ResultEntrySerializer, ResultVerificationSerializer
|
|
)
|
|
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 LabTestViewSet(BaseViewSet):
|
|
"""ViewSet for LabTest model"""
|
|
queryset = LabTest.objects.all()
|
|
serializer_class = LabTestSerializer
|
|
filterset_fields = [
|
|
'category', 'specimen_type', 'requires_fasting', 'is_active'
|
|
]
|
|
search_fields = [
|
|
'code', 'name', 'description', 'cpt_code', 'loinc_code'
|
|
]
|
|
ordering_fields = ['name', 'code', 'category', 'turnaround_time_hours']
|
|
ordering = ['name']
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def by_category(self, request):
|
|
"""Get tests grouped by category"""
|
|
category = request.query_params.get('category')
|
|
if category:
|
|
queryset = self.get_queryset().filter(category=category, is_active=True)
|
|
else:
|
|
queryset = self.get_queryset().filter(is_active=True)
|
|
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def search_by_code(self, request):
|
|
"""Search tests by code (CPT or LOINC)"""
|
|
code = request.query_params.get('code')
|
|
if not code:
|
|
return Response({'error': 'Code parameter required'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
queryset = self.get_queryset().filter(
|
|
Q(code__icontains=code) | Q(cpt_code__icontains=code) | Q(loinc_code__icontains=code),
|
|
is_active=True
|
|
)
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=['get'])
|
|
def reference_ranges(self, request, pk=None):
|
|
"""Get reference ranges for a test"""
|
|
test = self.get_object()
|
|
ranges = ReferenceRange.objects.filter(lab_test=test, is_active=True)
|
|
serializer = ReferenceRangeSerializer(ranges, many=True)
|
|
return Response(serializer.data)
|
|
|
|
|
|
class LabOrderViewSet(BaseViewSet):
|
|
"""ViewSet for LabOrder model"""
|
|
queryset = LabOrder.objects.all()
|
|
serializer_class = LabOrderSerializer
|
|
filterset_fields = [
|
|
'status', 'priority', 'patient', 'ordering_provider', 'fasting_required'
|
|
]
|
|
search_fields = [
|
|
'order_number', 'patient__first_name', 'patient__last_name',
|
|
'patient__mrn', 'ordering_provider__first_name', 'ordering_provider__last_name'
|
|
]
|
|
ordering_fields = ['order_date', 'collection_scheduled_date', 'priority']
|
|
ordering = ['-order_date']
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def pending(self, request):
|
|
"""Get pending orders"""
|
|
queryset = self.get_queryset().filter(status='PENDING')
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def scheduled_today(self, request):
|
|
"""Get orders scheduled for collection today"""
|
|
today = timezone.now().date()
|
|
queryset = self.get_queryset().filter(
|
|
collection_scheduled_date__date=today,
|
|
status__in=['PENDING', 'SCHEDULED']
|
|
)
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def schedule_collection(self, request, pk=None):
|
|
"""Schedule collection for an order"""
|
|
order = self.get_object()
|
|
collection_date = request.data.get('collection_date')
|
|
special_instructions = request.data.get('special_instructions', '')
|
|
|
|
if not collection_date:
|
|
return Response(
|
|
{'error': 'Collection date is required'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
order.collection_scheduled_date = collection_date
|
|
order.status = 'SCHEDULED'
|
|
order.special_instructions = special_instructions
|
|
order.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='ORDER_SCHEDULED',
|
|
model='LabOrder',
|
|
object_id=str(order.order_id),
|
|
details={
|
|
'order_number': order.order_number,
|
|
'collection_date': collection_date
|
|
}
|
|
)
|
|
|
|
return Response({'message': 'Collection scheduled successfully'})
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def cancel(self, request, pk=None):
|
|
"""Cancel an order"""
|
|
order = self.get_object()
|
|
reason = request.data.get('reason', '')
|
|
|
|
order.status = 'CANCELLED'
|
|
order.notes = f"Cancelled: {reason}"
|
|
order.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='ORDER_CANCELLED',
|
|
model='LabOrder',
|
|
object_id=str(order.order_id),
|
|
details={
|
|
'order_number': order.order_number,
|
|
'reason': reason
|
|
}
|
|
)
|
|
|
|
return Response({'message': 'Order cancelled successfully'})
|
|
|
|
|
|
class SpecimenViewSet(BaseViewSet):
|
|
"""ViewSet for Specimen model"""
|
|
queryset = Specimen.objects.all()
|
|
serializer_class = SpecimenSerializer
|
|
filterset_fields = [
|
|
'status', 'specimen_type', 'lab_order', 'collected_by', 'received_by'
|
|
]
|
|
search_fields = [
|
|
'specimen_number', 'lab_order__order_number', 'lab_order__patient__first_name',
|
|
'lab_order__patient__last_name', 'lab_order__patient__mrn'
|
|
]
|
|
ordering_fields = ['collection_date', 'received_date', 'processing_started_date']
|
|
ordering = ['-collection_date']
|
|
|
|
@action(detail=False, methods=['post'])
|
|
def collect(self, request):
|
|
"""Collect a specimen"""
|
|
serializer = SpecimenCollectionSerializer(data=request.data)
|
|
|
|
if serializer.is_valid():
|
|
order = LabOrder.objects.get(id=serializer.validated_data['order_id'])
|
|
|
|
# Create specimen
|
|
specimen = Specimen.objects.create(
|
|
lab_order=order,
|
|
specimen_type=serializer.validated_data['specimen_type'],
|
|
collection_date=timezone.now().date(),
|
|
collection_time=timezone.now().time(),
|
|
collected_by=request.user,
|
|
collection_site=serializer.validated_data['collection_site'],
|
|
collection_method=serializer.validated_data['collection_method'],
|
|
container_type=serializer.validated_data['container_type'],
|
|
volume_collected=serializer.validated_data['volume_collected'],
|
|
status='COLLECTED',
|
|
comments=serializer.validated_data.get('comments', ''),
|
|
tenant=getattr(request.user, 'tenant', None)
|
|
)
|
|
|
|
# Update order status
|
|
order.status = 'COLLECTED'
|
|
order.collection_date = timezone.now()
|
|
order.collected_by = request.user
|
|
order.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='SPECIMEN_COLLECTED',
|
|
model='Specimen',
|
|
object_id=str(specimen.specimen_id),
|
|
details={
|
|
'specimen_number': specimen.specimen_number,
|
|
'order_number': order.order_number
|
|
}
|
|
)
|
|
|
|
return Response({
|
|
'message': 'Specimen collected successfully',
|
|
'specimen': SpecimenSerializer(specimen).data
|
|
})
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def receive(self, request, pk=None):
|
|
"""Receive a specimen in the lab"""
|
|
specimen = self.get_object()
|
|
|
|
specimen.status = 'RECEIVED'
|
|
specimen.received_date = timezone.now()
|
|
specimen.received_by = request.user
|
|
specimen.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='SPECIMEN_RECEIVED',
|
|
model='Specimen',
|
|
object_id=str(specimen.specimen_id),
|
|
details={'specimen_number': specimen.specimen_number}
|
|
)
|
|
|
|
return Response({'message': 'Specimen received successfully'})
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def start_processing(self, request, pk=None):
|
|
"""Start processing a specimen"""
|
|
specimen = self.get_object()
|
|
|
|
specimen.status = 'PROCESSING'
|
|
specimen.processing_started_date = timezone.now()
|
|
specimen.processed_by = request.user
|
|
specimen.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='SPECIMEN_PROCESSING_STARTED',
|
|
model='Specimen',
|
|
object_id=str(specimen.specimen_id),
|
|
details={'specimen_number': specimen.specimen_number}
|
|
)
|
|
|
|
return Response({'message': 'Processing started successfully'})
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def reject(self, request, pk=None):
|
|
"""Reject a specimen"""
|
|
specimen = self.get_object()
|
|
reason = request.data.get('reason', '')
|
|
|
|
specimen.status = 'REJECTED'
|
|
specimen.rejection_reason = reason
|
|
specimen.rejected_by = request.user
|
|
specimen.rejected_date = timezone.now()
|
|
specimen.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='SPECIMEN_REJECTED',
|
|
model='Specimen',
|
|
object_id=str(specimen.specimen_id),
|
|
details={
|
|
'specimen_number': specimen.specimen_number,
|
|
'reason': reason
|
|
}
|
|
)
|
|
|
|
return Response({'message': 'Specimen rejected successfully'})
|
|
|
|
|
|
class LabResultViewSet(BaseViewSet):
|
|
"""ViewSet for LabResult model"""
|
|
queryset = LabResult.objects.all()
|
|
serializer_class = LabResultSerializer
|
|
filterset_fields = [
|
|
'status', 'result_status', 'lab_test', 'lab_order', 'critical_flag',
|
|
'abnormal_flag', 'performed_by', 'verified_by'
|
|
]
|
|
search_fields = [
|
|
'lab_order__order_number', 'lab_order__patient__first_name',
|
|
'lab_order__patient__last_name', 'lab_test__name', 'specimen__specimen_number'
|
|
]
|
|
ordering_fields = ['performed_date', 'verified_date']
|
|
ordering = ['-performed_date']
|
|
|
|
@action(detail=False, methods=['post'])
|
|
def enter_result(self, request):
|
|
"""Enter a lab result"""
|
|
serializer = ResultEntrySerializer(data=request.data)
|
|
|
|
if serializer.is_valid():
|
|
specimen = Specimen.objects.get(id=serializer.validated_data['specimen_id'])
|
|
lab_test = LabTest.objects.get(id=serializer.validated_data['lab_test_id'])
|
|
|
|
# Create result
|
|
result = LabResult.objects.create(
|
|
lab_order=specimen.lab_order,
|
|
lab_test=lab_test,
|
|
specimen=specimen,
|
|
result_value=serializer.validated_data.get('result_value', ''),
|
|
result_text=serializer.validated_data.get('result_text', ''),
|
|
abnormal_flag=serializer.validated_data.get('abnormal_flag', ''),
|
|
critical_flag=serializer.validated_data.get('critical_flag', False),
|
|
status='PENDING_VERIFICATION',
|
|
performed_date=timezone.now(),
|
|
performed_by=request.user,
|
|
instrument_id=serializer.validated_data.get('instrument_id', ''),
|
|
method_used=serializer.validated_data.get('method_used', ''),
|
|
comments=serializer.validated_data.get('comments', ''),
|
|
tenant=getattr(request.user, 'tenant', None)
|
|
)
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='RESULT_ENTERED',
|
|
model='LabResult',
|
|
object_id=str(result.result_id),
|
|
details={
|
|
'test_name': lab_test.name,
|
|
'specimen_number': specimen.specimen_number
|
|
}
|
|
)
|
|
|
|
return Response({
|
|
'message': 'Result entered successfully',
|
|
'result': LabResultSerializer(result).data
|
|
})
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def pending_verification(self, request):
|
|
"""Get results pending verification"""
|
|
queryset = self.get_queryset().filter(status='PENDING_VERIFICATION')
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def critical_results(self, request):
|
|
"""Get critical results"""
|
|
queryset = self.get_queryset().filter(critical_flag=True, status='VERIFIED')
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def verify(self, request, pk=None):
|
|
"""Verify a lab result"""
|
|
result = self.get_object()
|
|
comments = request.data.get('verification_comments', '')
|
|
|
|
result.status = 'VERIFIED'
|
|
result.verified_date = timezone.now()
|
|
result.verified_by = request.user
|
|
if comments:
|
|
result.comments = f"{result.comments}\nVerification: {comments}"
|
|
result.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='RESULT_VERIFIED',
|
|
model='LabResult',
|
|
object_id=str(result.result_id),
|
|
details={
|
|
'test_name': result.lab_test.name,
|
|
'specimen_number': result.specimen.specimen_number
|
|
}
|
|
)
|
|
|
|
return Response({'message': 'Result verified successfully'})
|
|
|
|
|
|
class QualityControlViewSet(BaseViewSet):
|
|
"""ViewSet for QualityControl model"""
|
|
queryset = QualityControl.objects.all()
|
|
serializer_class = QualityControlSerializer
|
|
filterset_fields = ['lab_test', 'control_type', 'status', 'performed_by']
|
|
search_fields = ['lab_test__name', 'control_lot', 'instrument_id']
|
|
ordering_fields = ['run_date']
|
|
ordering = ['-run_date']
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def failed_controls(self, request):
|
|
"""Get failed quality controls"""
|
|
queryset = self.get_queryset().filter(status='FAILED')
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def daily_summary(self, request):
|
|
"""Get daily QC summary"""
|
|
date = request.query_params.get('date', timezone.now().date())
|
|
queryset = self.get_queryset().filter(run_date__date=date)
|
|
|
|
total = queryset.count()
|
|
passed = queryset.filter(status='PASSED').count()
|
|
failed = queryset.filter(status='FAILED').count()
|
|
|
|
return Response({
|
|
'date': date,
|
|
'total_controls': total,
|
|
'passed': passed,
|
|
'failed': failed,
|
|
'pass_rate': (passed / total * 100) if total > 0 else 0
|
|
})
|
|
|
|
|
|
class ReferenceRangeViewSet(BaseViewSet):
|
|
"""ViewSet for ReferenceRange model"""
|
|
queryset = ReferenceRange.objects.all()
|
|
serializer_class = ReferenceRangeSerializer
|
|
filterset_fields = ['lab_test', 'gender', 'is_active']
|
|
search_fields = ['lab_test__name', 'population']
|
|
ordering_fields = ['effective_date', 'age_min']
|
|
ordering = ['lab_test__name', 'age_min']
|
|
|
|
|
|
class LabStatsViewSet(viewsets.ViewSet):
|
|
"""ViewSet for laboratory statistics"""
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def dashboard(self, request):
|
|
"""Get laboratory dashboard statistics"""
|
|
tenant_filter = {}
|
|
if hasattr(request.user, 'tenant') and request.user.tenant:
|
|
tenant_filter['tenant'] = request.user.tenant
|
|
|
|
today = timezone.now().date()
|
|
|
|
# Order statistics
|
|
orders = LabOrder.objects.filter(**tenant_filter)
|
|
total_orders = orders.count()
|
|
pending_orders = orders.filter(status='PENDING').count()
|
|
completed_today = orders.filter(
|
|
status='COMPLETED',
|
|
collection_date__date=today
|
|
).count()
|
|
|
|
# Specimen statistics
|
|
specimens = Specimen.objects.filter(**tenant_filter)
|
|
specimens_collected = specimens.filter(
|
|
collection_date=today
|
|
).count()
|
|
|
|
# Result statistics
|
|
results = LabResult.objects.filter(**tenant_filter)
|
|
results_pending = results.filter(status='PENDING_VERIFICATION').count()
|
|
critical_results = results.filter(
|
|
critical_flag=True,
|
|
status='VERIFIED',
|
|
verified_date__date=today
|
|
).count()
|
|
|
|
# Turnaround time
|
|
completed_results = results.filter(
|
|
status='VERIFIED',
|
|
verified_date__date=today
|
|
)
|
|
turnaround_times = []
|
|
for result in completed_results:
|
|
if result.lab_order.collection_date and result.verified_date:
|
|
delta = result.verified_date - result.lab_order.collection_date
|
|
turnaround_times.append(delta.total_seconds() / 3600) # hours
|
|
|
|
turnaround_time_avg = sum(turnaround_times) / len(turnaround_times) if turnaround_times else 0
|
|
|
|
# QC pass rate
|
|
qc_today = QualityControl.objects.filter(
|
|
run_date__date=today,
|
|
**tenant_filter
|
|
)
|
|
qc_total = qc_today.count()
|
|
qc_passed = qc_today.filter(status='PASSED').count()
|
|
qc_pass_rate = (qc_passed / qc_total * 100) if qc_total > 0 else 100
|
|
|
|
# Order types breakdown
|
|
order_types = orders.values('status').annotate(
|
|
count=Count('id')
|
|
).order_by('-count')
|
|
|
|
stats = {
|
|
'total_orders': total_orders,
|
|
'pending_orders': pending_orders,
|
|
'completed_today': completed_today,
|
|
'specimens_collected': specimens_collected,
|
|
'results_pending': results_pending,
|
|
'critical_results': critical_results,
|
|
'turnaround_time_avg': round(turnaround_time_avg, 2),
|
|
'qc_pass_rate': round(qc_pass_rate, 2),
|
|
'order_types': {item['status']: item['count'] for item in order_types},
|
|
'status_breakdown': dict(order_types.values_list('status', 'count'))
|
|
}
|
|
|
|
serializer = LabStatsSerializer(stats)
|
|
return Response(serializer.data)
|
|
|