""" Physician Rating Import Views UI views for manual CSV upload of doctor ratings. Similar to HIS Patient Import flow. """ import csv import io import logging from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db import transaction from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.views.decorators.http import require_http_methods, require_POST from apps.core.services import AuditService from apps.organizations.models import Hospital from .adapter import DoctorRatingAdapter from .forms import DoctorRatingImportForm from .models import DoctorRatingImportJob, PhysicianIndividualRating from .tasks import process_doctor_rating_job logger = logging.getLogger(__name__) @login_required def doctor_rating_import(request): """ Import doctor ratings from CSV (Doctor Rating Report format). CSV Format (Doctor Rating Report): - Header rows (rows 1-6 contain metadata) - Column headers in row 7: UHID, Patient Name, Gender, Full Age, Nationality, Mobile No, Patient Type, Admit Date, Discharge Date, Doctor Name, Rating, Feed Back, Rating Date - Department headers appear as rows with only first column filled - Data rows follow """ user = request.user # Check permission if not user.is_px_admin() and not user.is_hospital_admin(): messages.error(request, "You don't have permission to import doctor ratings.") return redirect('physicians:physician_list') # Session storage for imported ratings session_key = f'doctor_rating_import_{user.id}' if request.method == 'POST': form = DoctorRatingImportForm(user, request.POST, request.FILES) if form.is_valid(): try: hospital = form.cleaned_data['hospital'] csv_file = form.cleaned_data['csv_file'] skip_rows = form.cleaned_data['skip_header_rows'] # Parse CSV decoded_file = csv_file.read().decode('utf-8-sig') io_string = io.StringIO(decoded_file) reader = csv.reader(io_string) # Skip header/metadata rows for _ in range(skip_rows): next(reader, None) # Read header row header = next(reader, None) if not header: messages.error(request, "CSV file is empty or has no data rows.") return render(request, 'physicians/doctor_rating_import.html', {'form': form}) # Find column indices (handle different possible column names) header = [h.strip().lower() for h in header] col_map = { 'uhid': _find_column(header, ['uhid', 'file number', 'file_number', 'mrn', 'patient id']), 'patient_name': _find_column(header, ['patient name', 'patient_name', 'name']), 'gender': _find_column(header, ['gender', 'sex']), 'age': _find_column(header, ['full age', 'age', 'years']), 'nationality': _find_column(header, ['nationality', 'country']), 'mobile_no': _find_column(header, ['mobile no', 'mobile_no', 'mobile', 'phone', 'contact']), 'patient_type': _find_column(header, ['patient type', 'patient_type', 'type', 'visit type']), 'admit_date': _find_column(header, ['admit date', 'admit_date', 'admission date', 'visit date']), 'discharge_date': _find_column(header, ['discharge date', 'discharge_date']), 'doctor_name': _find_column(header, ['doctor name', 'doctor_name', 'physician name', 'physician']), 'rating': _find_column(header, ['rating', 'score', 'rate']), 'feedback': _find_column(header, ['feed back', 'feedback', 'comments', 'comment']), 'rating_date': _find_column(header, ['rating date', 'rating_date', 'date']), 'department': _find_column(header, ['department', 'dept', 'specialty']), } # Check required columns if col_map['uhid'] is None: messages.error(request, "Could not find 'UHID' column in CSV.") return render(request, 'physicians/doctor_rating_import.html', {'form': form}) if col_map['doctor_name'] is None: messages.error(request, "Could not find 'Doctor Name' column in CSV.") return render(request, 'physicians/doctor_rating_import.html', {'form': form}) if col_map['rating'] is None: messages.error(request, "Could not find 'Rating' column in CSV.") return render(request, 'physicians/doctor_rating_import.html', {'form': form}) # Process data rows imported_ratings = [] errors = [] row_num = skip_rows + 1 current_department = "" for row in reader: row_num += 1 if not row or not any(row): # Skip empty rows continue try: # Check if this is a department header row (only first column has value) if _is_department_header(row, col_map): current_department = row[0].strip() continue # Extract data uhid = _get_cell(row, col_map['uhid'], '').strip() patient_name = _get_cell(row, col_map['patient_name'], '').strip() doctor_name_raw = _get_cell(row, col_map['doctor_name'], '').strip() rating_str = _get_cell(row, col_map['rating'], '').strip() # Skip if missing required fields if not uhid or not doctor_name_raw: continue # Validate rating try: rating = int(float(rating_str)) if rating < 1 or rating > 5: errors.append(f"Row {row_num}: Invalid rating {rating}") continue except (ValueError, TypeError): errors.append(f"Row {row_num}: Invalid rating format '{rating_str}'") continue # Extract optional fields gender = _get_cell(row, col_map['gender'], '').strip() age = _get_cell(row, col_map['age'], '').strip() nationality = _get_cell(row, col_map['nationality'], '').strip() mobile_no = _get_cell(row, col_map['mobile_no'], '').strip() patient_type = _get_cell(row, col_map['patient_type'], '').strip() admit_date = _get_cell(row, col_map['admit_date'], '').strip() discharge_date = _get_cell(row, col_map['discharge_date'], '').strip() feedback = _get_cell(row, col_map['feedback'], '').strip() rating_date = _get_cell(row, col_map['rating_date'], '').strip() department = _get_cell(row, col_map['department'], '').strip() or current_department # Parse doctor name to extract ID doctor_id, doctor_name_clean = DoctorRatingAdapter.parse_doctor_name(doctor_name_raw) imported_ratings.append({ 'row_num': row_num, 'uhid': uhid, 'patient_name': patient_name, 'doctor_name_raw': doctor_name_raw, 'doctor_id': doctor_id, 'doctor_name': doctor_name_clean, 'rating': rating, 'gender': gender, 'age': age, 'nationality': nationality, 'mobile_no': mobile_no, 'patient_type': patient_type, 'admit_date': admit_date, 'discharge_date': discharge_date, 'feedback': feedback, 'rating_date': rating_date, 'department': department, }) except Exception as e: errors.append(f"Row {row_num}: {str(e)}") logger.error(f"Error processing row {row_num}: {e}") # Store in session for review step request.session[session_key] = { 'hospital_id': str(hospital.id), 'ratings': imported_ratings, 'errors': errors, 'total_count': len(imported_ratings) } # Log audit AuditService.log_event( event_type='doctor_rating_csv_import', description=f"Parsed {len(imported_ratings)} doctor ratings from CSV by {user.get_full_name()}", user=user, metadata={ 'hospital': hospital.name, 'total_count': len(imported_ratings), 'error_count': len(errors) } ) if imported_ratings: messages.success( request, f"Successfully parsed {len(imported_ratings)} doctor rating records. Please review before importing." ) return redirect('physicians:doctor_rating_review') else: messages.error(request, "No valid doctor rating records found in CSV.") except Exception as e: logger.error(f"Error processing Doctor Rating CSV: {str(e)}", exc_info=True) messages.error(request, f"Error processing CSV: {str(e)}") else: form = DoctorRatingImportForm(user) context = { 'form': form, } return render(request, 'physicians/doctor_rating_import.html', context) @login_required def doctor_rating_review(request): """ Review imported doctor ratings before creating records. """ user = request.user session_key = f'doctor_rating_import_{user.id}' import_data = request.session.get(session_key) if not import_data: messages.error(request, "No import data found. Please upload CSV first.") return redirect('physicians:doctor_rating_import') hospital = get_object_or_404(Hospital, id=import_data['hospital_id']) ratings = import_data['ratings'] errors = import_data.get('errors', []) # Check for staff matches for r in ratings: staff = DoctorRatingAdapter.find_staff_by_doctor_id( r['doctor_id'], hospital, r['doctor_name'] ) r['staff_matched'] = staff is not None r['staff_name'] = staff.get_full_name() if staff else None if request.method == 'POST': action = request.POST.get('action') if action == 'import': # Queue bulk import job job = DoctorRatingImportJob.objects.create( name=f"CSV Import - {hospital.name} - {len(ratings)} ratings", status=DoctorRatingImportJob.JobStatus.PENDING, source=DoctorRatingImportJob.JobSource.CSV_UPLOAD, created_by=user, hospital=hospital, total_records=len(ratings), raw_data=ratings ) # Queue the background task process_doctor_rating_job.delay(str(job.id)) # Log audit AuditService.log_event( event_type='doctor_rating_import_queued', description=f"Queued {len(ratings)} doctor ratings for import", user=user, metadata={ 'job_id': str(job.id), 'hospital': hospital.name, 'total_records': len(ratings) } ) # Clear session del request.session[session_key] messages.success( request, f"Import job queued for {len(ratings)} ratings. You can check the status below." ) return redirect('physicians:doctor_rating_job_status', job_id=job.id) elif action == 'cancel': del request.session[session_key] messages.info(request, "Import cancelled.") return redirect('physicians:doctor_rating_import') # Pagination paginator = Paginator(ratings, 50) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) context = { 'hospital': hospital, 'ratings': ratings, 'page_obj': page_obj, 'errors': errors, 'total_count': len(ratings), 'matched_count': sum(1 for r in ratings if r['staff_matched']), 'unmatched_count': sum(1 for r in ratings if not r['staff_matched']), } return render(request, 'physicians/doctor_rating_review.html', context) @login_required def doctor_rating_job_status(request, job_id): """ View status of a doctor rating import job. """ user = request.user job = get_object_or_404(DoctorRatingImportJob, id=job_id) # Check permission if not user.is_px_admin() and job.hospital != user.hospital: messages.error(request, "You don't have permission to view this job.") return redirect('physicians:physician_list') context = { 'job': job, 'progress': job.progress_percentage, 'is_complete': job.is_complete, 'results': job.results, } return render(request, 'physicians/doctor_rating_job_status.html', context) @login_required def doctor_rating_job_list(request): """ List all doctor rating import jobs for the user. """ user = request.user # Filter jobs if user.is_px_admin(): jobs = DoctorRatingImportJob.objects.all() elif user.hospital: jobs = DoctorRatingImportJob.objects.filter(hospital=user.hospital) else: jobs = DoctorRatingImportJob.objects.filter(created_by=user) jobs = jobs.order_by('-created_at')[:50] # Last 50 jobs context = { 'jobs': jobs, } return render(request, 'physicians/doctor_rating_job_list.html', context) @login_required def individual_ratings_list(request): """ List individual doctor ratings with filtering. """ user = request.user # Base queryset queryset = PhysicianIndividualRating.objects.select_related( 'hospital', 'staff', 'staff__department' ) # Apply RBAC if not user.is_px_admin(): if user.hospital: queryset = queryset.filter(hospital=user.hospital) else: queryset = queryset.none() # Filters hospital_id = request.GET.get('hospital') doctor_id = request.GET.get('doctor_id') rating_min = request.GET.get('rating_min') rating_max = request.GET.get('rating_max') date_from = request.GET.get('date_from') date_to = request.GET.get('date_to') source = request.GET.get('source') if hospital_id: queryset = queryset.filter(hospital_id=hospital_id) if doctor_id: queryset = queryset.filter(doctor_id=doctor_id) if rating_min: queryset = queryset.filter(rating__gte=int(rating_min)) if rating_max: queryset = queryset.filter(rating__lte=int(rating_max)) if date_from: queryset = queryset.filter(rating_date__date__gte=date_from) if date_to: queryset = queryset.filter(rating_date__date__lte=date_to) if source: queryset = queryset.filter(source=source) # Ordering queryset = queryset.order_by('-rating_date') # Pagination paginator = Paginator(queryset, 25) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) # Get hospitals for filter from apps.organizations.models import Hospital if user.is_px_admin(): hospitals = Hospital.objects.filter(status='active') else: hospitals = Hospital.objects.filter(id=user.hospital.id) if user.hospital else Hospital.objects.none() context = { 'page_obj': page_obj, 'hospitals': hospitals, 'sources': PhysicianIndividualRating.RatingSource.choices, 'filters': { 'hospital': hospital_id, 'doctor_id': doctor_id, 'rating_min': rating_min, 'rating_max': rating_max, 'date_from': date_from, 'date_to': date_to, 'source': source, } } return render(request, 'physicians/individual_ratings_list.html', context) # ============================================================================ # Helper Functions # ============================================================================ def _find_column(header, possible_names): """Find column index by possible names.""" for name in possible_names: for i, h in enumerate(header): if name.lower() in h.lower(): return i return None def _get_cell(row, index, default=''): """Safely get cell value.""" if index is None or index >= len(row): return default return row[index].strip() if row[index] else default def _is_department_header(row, col_map): """ Check if a row is a department header row. Department headers typically have: - First column has text (department name) - All other columns are empty """ if not row or not row[0]: return False # Check if first column has text first_col = row[0].strip() if not first_col: return False # Check if other important columns are empty # If UHID, Doctor Name, Rating are all empty, it's likely a header uhid = _get_cell(row, col_map.get('uhid'), '').strip() doctor_name = _get_cell(row, col_map.get('doctor_name'), '').strip() rating = _get_cell(row, col_map.get('rating'), '').strip() # If these key fields are empty but first column has text, it's a department header if not uhid and not doctor_name and not rating: return True return False # ============================================================================ # AJAX Endpoints # ============================================================================ @login_required def api_job_progress(request, job_id): """AJAX endpoint to get job progress.""" user = request.user job = get_object_or_404(DoctorRatingImportJob, id=job_id) # Check permission if not user.is_px_admin() and job.hospital != user.hospital: return JsonResponse({'error': 'Permission denied'}, status=403) return JsonResponse({ 'job_id': str(job.id), 'status': job.status, 'progress_percentage': job.progress_percentage, 'processed_count': job.processed_count, 'total_records': job.total_records, 'success_count': job.success_count, 'failed_count': job.failed_count, 'is_complete': job.is_complete, }) @login_required def api_match_doctor(request): """ AJAX endpoint to manually match a doctor to a staff record. POST data: - doctor_id: The doctor ID from the rating - doctor_name: The doctor name - staff_id: The staff ID to match to """ if request.method != 'POST': return JsonResponse({'error': 'POST required'}, status=405) user = request.user doctor_id = request.POST.get('doctor_id') doctor_name = request.POST.get('doctor_name') staff_id = request.POST.get('staff_id') if not staff_id: return JsonResponse({'error': 'staff_id required'}, status=400) try: staff = Staff.objects.get(id=staff_id) # Check permission if not user.is_px_admin() and staff.hospital != user.hospital: return JsonResponse({'error': 'Permission denied'}, status=403) # Update all unaggregated ratings for this doctor count = PhysicianIndividualRating.objects.filter( doctor_id=doctor_id, is_aggregated=False ).update(staff=staff) return JsonResponse({ 'success': True, 'matched_count': count, 'staff_name': staff.get_full_name() }) except Staff.DoesNotExist: return JsonResponse({'error': 'Staff not found'}, status=404) except Exception as e: logger.error(f"Error matching doctor: {str(e)}", exc_info=True) return JsonResponse({'error': str(e)}, status=500)