593 lines
20 KiB
Python
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
|
|
)
|