HH/apps/physicians/api_views.py
2026-02-22 08:35:53 +03:00

593 lines
20 KiB
Python

"""
Physicians API Views - Doctor Rating Import
API endpoints for HIS integration and manual rating import.
"""
import logging
from django.shortcuts import get_object_or_404
from rest_framework import status, serializers
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.authentication import TokenAuthentication
from apps.accounts.permissions import IsPXAdminOrHospitalAdmin
from apps.core.services import AuditService
from apps.organizations.models import Hospital
from .adapter import DoctorRatingAdapter
from .models import DoctorRatingImportJob, PhysicianIndividualRating
from .tasks import process_doctor_rating_job, aggregate_monthly_ratings_task
logger = logging.getLogger(__name__)
# ============================================================================
# Serializers
# ============================================================================
class DoctorRatingImportSerializer(serializers.Serializer):
"""Serializer for single doctor rating import via API."""
uhid = serializers.CharField(required=True, help_text="Patient UHID/MRN")
patient_name = serializers.CharField(required=True)
gender = serializers.CharField(required=False, allow_blank=True)
age = serializers.CharField(required=False, allow_blank=True)
nationality = serializers.CharField(required=False, allow_blank=True)
mobile_no = serializers.CharField(required=False, allow_blank=True)
patient_type = serializers.CharField(required=False, allow_blank=True, help_text="IP, OP, ER, DC")
admit_date = serializers.CharField(required=False, allow_blank=True, help_text="Format: DD-MMM-YYYY HH:MM:SS")
discharge_date = serializers.CharField(required=False, allow_blank=True, help_text="Format: DD-MMM-YYYY HH:MM:SS")
doctor_name = serializers.CharField(required=True, help_text="Format: ID-NAME (e.g., '10738-OMAYMAH YAQOUB')")
rating = serializers.IntegerField(required=True, min_value=1, max_value=5)
feedback = serializers.CharField(required=False, allow_blank=True)
rating_date = serializers.CharField(required=True, help_text="Format: DD-MMM-YYYY HH:MM:SS")
department = serializers.CharField(required=False, allow_blank=True)
class BulkDoctorRatingImportSerializer(serializers.Serializer):
"""Serializer for bulk doctor rating import via API."""
hospital_id = serializers.UUIDField(required=True)
ratings = DoctorRatingImportSerializer(many=True, required=True)
source_reference = serializers.CharField(required=False, allow_blank=True, help_text="Reference ID from HIS system")
class DoctorRatingResponseSerializer(serializers.Serializer):
"""Serializer for doctor rating import response."""
success = serializers.BooleanField()
rating_id = serializers.UUIDField(required=False)
message = serializers.CharField(required=False)
staff_matched = serializers.BooleanField(required=False)
staff_id = serializers.UUIDField(required=False)
# ============================================================================
# API Endpoints
# ============================================================================
@api_view(['POST'])
@authentication_classes([TokenAuthentication])
@permission_classes([IsAuthenticated])
def import_single_rating(request):
"""
Import a single doctor rating from HIS.
POST /api/physicians/ratings/import/single/
Expected payload:
{
"hospital_id": "uuid",
"uhid": "ALHH.0030223126",
"patient_name": "Tamam Saud Aljunaybi",
"gender": "Female",
"age": "36 Years",
"nationality": "Saudi Arabia",
"mobile_no": "0504884011",
"patient_type": "OP",
"admit_date": "22-Dec-2024 19:12:24",
"discharge_date": "",
"doctor_name": "10738-OMAYMAH YAQOUB ELAMEIAN",
"rating": 5,
"feedback": "Great service",
"rating_date": "28-Dec-2024 22:31:29",
"department": "ACCIDENT AND EMERGENCY"
}
Returns:
{
"success": true,
"rating_id": "uuid",
"message": "Rating imported successfully",
"staff_matched": true,
"staff_id": "uuid"
}
"""
try:
# Validate request data
serializer = DoctorRatingImportSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{'success': False, 'errors': serializer.errors},
status=status.HTTP_400_BAD_REQUEST
)
data = serializer.validated_data
# Get hospital
hospital_id = request.data.get('hospital_id')
if not hospital_id:
return Response(
{'success': False, 'message': 'hospital_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
hospital = get_object_or_404(Hospital, id=hospital_id)
# Check permission
user = request.user
if not user.is_px_admin() and user.hospital != hospital:
return Response(
{'success': False, 'message': 'Permission denied for this hospital'},
status=status.HTTP_403_FORBIDDEN
)
# Process the rating
result = DoctorRatingAdapter.process_single_rating(
data=data,
hospital=hospital,
source=PhysicianIndividualRating.RatingSource.HIS_API
)
# Log audit
if result['success']:
AuditService.log_event(
event_type='doctor_rating_import',
description=f"Doctor rating imported for {data.get('doctor_name')}",
user=user,
metadata={
'hospital': hospital.name,
'doctor_name': data.get('doctor_name'),
'rating': data.get('rating'),
'staff_matched': result['staff_matched']
}
)
return Response(result, status=status.HTTP_201_CREATED)
else:
return Response(result, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"Error importing doctor rating: {str(e)}", exc_info=True)
return Response(
{'success': False, 'message': f"Server error: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['POST'])
@authentication_classes([TokenAuthentication])
@permission_classes([IsAuthenticated])
def import_bulk_ratings(request):
"""
Import multiple doctor ratings from HIS (background processing).
POST /api/physicians/ratings/import/bulk/
Expected payload:
{
"hospital_id": "uuid",
"source_reference": "HIS_BATCH_20240115_001",
"ratings": [
{
"uhid": "ALHH.0030223126",
"patient_name": "Tamam Saud Aljunaybi",
"doctor_name": "10738-OMAYMAH YAQOUB ELAMEIAN",
"rating": 5,
"rating_date": "28-Dec-2024 22:31:29",
...
},
...
]
}
Returns:
{
"success": true,
"job_id": "uuid",
"job_status": "pending",
"message": "Bulk import job queued",
"total_records": 150
}
"""
try:
# Validate request data
serializer = BulkDoctorRatingImportSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{'success': False, 'errors': serializer.errors},
status=status.HTTP_400_BAD_REQUEST
)
data = serializer.validated_data
hospital = get_object_or_404(Hospital, id=data['hospital_id'])
# Check permission
user = request.user
if not user.is_px_admin() and user.hospital != hospital:
return Response(
{'success': False, 'message': 'Permission denied for this hospital'},
status=status.HTTP_403_FORBIDDEN
)
# Create import job
ratings = data['ratings']
job = DoctorRatingImportJob.objects.create(
name=f"HIS Bulk Import - {hospital.name} - {len(ratings)} records",
status=DoctorRatingImportJob.JobStatus.PENDING,
source=DoctorRatingImportJob.JobSource.HIS_API,
created_by=user,
hospital=hospital,
total_records=len(ratings),
raw_data=[dict(r) for r in ratings],
results={'source_reference': data.get('source_reference', '')}
)
# Queue background task
process_doctor_rating_job.delay(str(job.id))
# Log audit
AuditService.log_event(
event_type='doctor_rating_bulk_import',
description=f"Bulk doctor rating import queued: {len(ratings)} records",
user=user,
metadata={
'hospital': hospital.name,
'job_id': str(job.id),
'total_records': len(ratings),
'source_reference': data.get('source_reference', '')
}
)
return Response({
'success': True,
'job_id': str(job.id),
'job_status': job.status,
'message': 'Bulk import job queued for processing',
'total_records': len(ratings)
}, status=status.HTTP_202_ACCEPTED)
except Exception as e:
logger.error(f"Error queuing bulk doctor rating import: {str(e)}", exc_info=True)
return Response(
{'success': False, 'message': f"Server error: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def import_job_status(request, job_id):
"""
Get status of a doctor rating import job.
GET /api/physicians/ratings/import/jobs/{job_id}/
Returns:
{
"job_id": "uuid",
"name": "HIS Bulk Import - ...",
"status": "completed",
"progress_percentage": 100,
"total_records": 150,
"processed_count": 150,
"success_count": 145,
"failed_count": 5,
"started_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:35:00Z",
"duration_seconds": 300,
"results": {...}
}
"""
try:
job = get_object_or_404(DoctorRatingImportJob, id=job_id)
# Check permission
user = request.user
if not user.is_px_admin() and job.hospital != user.hospital:
return Response(
{'success': False, 'message': 'Permission denied'},
status=status.HTTP_403_FORBIDDEN
)
return Response({
'job_id': str(job.id),
'name': job.name,
'status': job.status,
'progress_percentage': job.progress_percentage,
'total_records': job.total_records,
'processed_count': job.processed_count,
'success_count': job.success_count,
'failed_count': job.failed_count,
'skipped_count': job.skipped_count,
'is_complete': job.is_complete,
'started_at': job.started_at,
'completed_at': job.completed_at,
'duration_seconds': job.duration_seconds,
'results': job.results,
'error_message': job.error_message
})
except Exception as e:
logger.error(f"Error getting import job status: {str(e)}", exc_info=True)
return Response(
{'success': False, 'message': f"Server error: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def import_job_list(request):
"""
List doctor rating import jobs for the user's hospital.
GET /api/physicians/ratings/import/jobs/?hospital_id={uuid}&limit=50
Returns list of jobs with status and progress.
"""
try:
user = request.user
hospital_id = request.query_params.get('hospital_id')
limit = int(request.query_params.get('limit', 50))
# Build queryset
queryset = DoctorRatingImportJob.objects.all()
if not user.is_px_admin():
if user.hospital:
queryset = queryset.filter(hospital=user.hospital)
else:
queryset = queryset.filter(created_by=user)
if hospital_id:
queryset = queryset.filter(hospital_id=hospital_id)
queryset = queryset.order_by('-created_at')[:limit]
jobs = []
for job in queryset:
jobs.append({
'job_id': str(job.id),
'name': job.name,
'status': job.status,
'source': job.source,
'progress_percentage': job.progress_percentage,
'total_records': job.total_records,
'success_count': job.success_count,
'failed_count': job.failed_count,
'is_complete': job.is_complete,
'created_at': job.created_at,
'hospital_name': job.hospital.name
})
return Response({'jobs': jobs})
except Exception as e:
logger.error(f"Error listing import jobs: {str(e)}", exc_info=True)
return Response(
{'success': False, 'message': f"Server error: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['POST'])
@permission_classes([IsPXAdminOrHospitalAdmin])
def trigger_monthly_aggregation(request):
"""
Trigger monthly aggregation of individual ratings.
POST /api/physicians/ratings/aggregate/
Expected payload:
{
"year": 2024,
"month": 12,
"hospital_id": "uuid" // optional
}
Returns:
{
"success": true,
"task_id": "celery-task-id",
"message": "Monthly aggregation queued"
}
"""
try:
year = request.data.get('year')
month = request.data.get('month')
hospital_id = request.data.get('hospital_id')
if not year or not month:
return Response(
{'success': False, 'message': 'year and month are required'},
status=status.HTTP_400_BAD_REQUEST
)
hospital = None
if hospital_id:
hospital = get_object_or_404(Hospital, id=hospital_id)
# Check permission
user = request.user
if not user.is_px_admin():
if hospital and hospital != user.hospital:
return Response(
{'success': False, 'message': 'Permission denied'},
status=status.HTTP_403_FORBIDDEN
)
if not hospital and not user.hospital:
return Response(
{'success': False, 'message': 'hospital_id required'},
status=status.HTTP_400_BAD_REQUEST
)
# Queue aggregation task
task = aggregate_monthly_ratings_task.delay(
year=int(year),
month=int(month),
hospital_id=str(hospital.id) if hospital else None
)
return Response({
'success': True,
'task_id': task.id,
'message': f'Monthly aggregation queued for {year}-{month:02d}'
})
except Exception as e:
logger.error(f"Error triggering aggregation: {str(e)}", exc_info=True)
return Response(
{'success': False, 'message': f"Server error: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# ============================================================================
# Simple HIS-compatible endpoint (similar to patient HIS endpoint)
# ============================================================================
@api_view(['POST'])
def his_doctor_rating_handler(request):
"""
HIS Doctor Rating API Endpoint - Compatible with HIS format.
This endpoint is designed to be compatible with HIS system integration,
accepting data in the same format as the Doctor Rating Report CSV.
POST /api/physicians/ratings/his/
Expected payload (single or array):
{
"hospital_code": "ALHH",
"ratings": [
{
"UHID": "ALHH.0030223126",
"PatientName": "Tamam Saud Aljunaybi",
"Gender": "Female",
"Age": "36 Years",
"Nationality": "Saudi Arabia",
"MobileNo": "0504884011",
"PatientType": "OP",
"AdmitDate": "22-Dec-2024 19:12:24",
"DischargeDate": "",
"DoctorName": "10738-OMAYMAH YAQOUB ELAMEIAN",
"Rating": 5,
"Feedback": "Great service",
"RatingDate": "28-Dec-2024 22:31:29",
"Department": "ACCIDENT AND EMERGENCY"
}
],
"source_reference": "HIS_BATCH_001"
}
Or simplified single record:
{
"hospital_code": "ALHH",
"UHID": "ALHH.0030223126",
"PatientName": "Tamam Saud Aljunaybi",
...
}
Returns:
{
"success": true,
"processed": 1,
"failed": 0,
"results": [...]
}
"""
try:
data = request.data
# Get hospital code
hospital_code = data.get('hospital_code')
if not hospital_code:
return Response(
{'success': False, 'message': 'hospital_code is required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
hospital = Hospital.objects.get(code__iexact=hospital_code)
except Hospital.DoesNotExist:
return Response(
{'success': False, 'message': f'Hospital with code {hospital_code} not found'},
status=status.HTTP_404_NOT_FOUND
)
# Normalize input to list of ratings
if 'ratings' in data:
ratings_list = data['ratings']
else:
# Single record format
ratings_list = [data]
# Map field names (HIS format -> internal format)
field_mapping = {
'UHID': 'uhid',
'PatientName': 'patient_name',
'Gender': 'gender',
'FullAge': 'age',
'Age': 'age',
'Nationality': 'nationality',
'MobileNo': 'mobile_no',
'PatientType': 'patient_type',
'AdmitDate': 'admit_date',
'DischargeDate': 'discharge_date',
'DoctorName': 'doctor_name',
'Rating': 'rating',
'FeedBack': 'feedback',
'Feedback': 'feedback',
'RatingDate': 'rating_date',
'Department': 'department',
}
results = []
success_count = 0
failed_count = 0
for record in ratings_list:
# Map fields
mapped_record = {}
for his_field, internal_field in field_mapping.items():
if his_field in record:
mapped_record[internal_field] = record[his_field]
# Process the rating
result = DoctorRatingAdapter.process_single_rating(
data=mapped_record,
hospital=hospital,
source=PhysicianIndividualRating.RatingSource.HIS_API,
source_reference=data.get('source_reference', '')
)
results.append(result)
if result['success']:
success_count += 1
else:
failed_count += 1
return Response({
'success': True,
'processed': success_count,
'failed': failed_count,
'total': len(ratings_list),
'results': results
})
except Exception as e:
logger.error(f"Error in HIS doctor rating handler: {str(e)}", exc_info=True)
return Response(
{'success': False, 'message': f"Server error: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)