""" HIS Patient Import Views Handles importing patient data from HIS/MOH Statistics CSV and sending surveys to imported patients. """ import csv import io import logging from datetime import datetime from django.contrib import messages from django.utils import timezone from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db import transaction from django.db.models import Q 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, Patient from apps.surveys.forms import HISPatientImportForm, HISSurveySendForm from apps.surveys.models import SurveyInstance, SurveyStatus, SurveyTemplate, BulkSurveyJob from apps.surveys.services import SurveyDeliveryService from apps.surveys.tasks import send_bulk_surveys logger = logging.getLogger(__name__) @login_required def his_patient_import(request): """ Import patients from HIS/MOH Statistics CSV. CSV Format (MOH Statistics): SNo, Facility Name, Visit Type, Admit Date, Discharge Date, Patient Name, File Number, SSN, Mobile No, Payment Type, Gender, Full Age, Nationality, Date of Birth, Diagnosis """ 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 patient data.") return redirect('surveys:instance_list') # Session storage for imported patients session_key = f'his_import_{user.id}' if request.method == 'POST': form = HISPatientImportForm(request.POST, request.FILES, user=user) 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, 'surveys/his_patient_import.html', {'form': form}) # Find column indices header = [h.strip().lower() for h in header] col_map = { 'file_number': _find_column(header, ['file number', 'file_number', 'mrn', 'file no']), 'patient_name': _find_column(header, ['patient name', 'patient_name', 'name']), 'mobile_no': _find_column(header, ['mobile no', 'mobile_no', 'mobile', 'phone']), 'ssn': _find_column(header, ['ssn', 'national id', 'national_id', 'id number']), 'gender': _find_column(header, ['gender', 'sex']), 'visit_type': _find_column(header, ['visit type', 'visit_type', 'type']), 'admit_date': _find_column(header, ['admit date', 'admit_date', 'admission date']), 'discharge_date': _find_column(header, ['discharge date', 'discharge_date']), 'facility': _find_column(header, ['facility name', 'facility', 'hospital']), 'nationality': _find_column(header, ['nationality', 'country']), 'dob': _find_column(header, ['date of birth', 'dob', 'birth date']), } # Check required columns if col_map['file_number'] is None: messages.error(request, "Could not find 'File Number' column in CSV.") return render(request, 'surveys/his_patient_import.html', {'form': form}) if col_map['patient_name'] is None: messages.error(request, "Could not find 'Patient Name' column in CSV.") return render(request, 'surveys/his_patient_import.html', {'form': form}) # Process data rows imported_patients = [] errors = [] row_num = skip_rows + 1 for row in reader: row_num += 1 if not row or not any(row): # Skip empty rows continue try: # Extract data file_number = _get_cell(row, col_map['file_number'], '').strip() patient_name = _get_cell(row, col_map['patient_name'], '').strip() mobile_no = _get_cell(row, col_map['mobile_no'], '').strip() ssn = _get_cell(row, col_map['ssn'], '').strip() gender = _get_cell(row, col_map['gender'], '').strip().lower() visit_type = _get_cell(row, col_map['visit_type'], '').strip() admit_date = _get_cell(row, col_map['admit_date'], '').strip() discharge_date = _get_cell(row, col_map['discharge_date'], '').strip() facility = _get_cell(row, col_map['facility'], '').strip() nationality = _get_cell(row, col_map['nationality'], '').strip() dob = _get_cell(row, col_map['dob'], '').strip() # Skip if missing required fields if not file_number or not patient_name: continue # Clean phone number if mobile_no: mobile_no = mobile_no.replace(' ', '').replace('-', '') if not mobile_no.startswith('+'): # Assume Saudi number if starts with 0 if mobile_no.startswith('05'): mobile_no = '+966' + mobile_no[1:] elif mobile_no.startswith('5'): mobile_no = '+966' + mobile_no # Parse name (First Middle Last format) name_parts = patient_name.split() first_name = name_parts[0] if name_parts else '' last_name = name_parts[-1] if len(name_parts) > 1 else '' # Parse dates parsed_admit = _parse_date(admit_date) parsed_discharge = _parse_date(discharge_date) parsed_dob = _parse_date(dob) # Normalize gender if gender in ['male', 'm']: gender = 'male' elif gender in ['female', 'f']: gender = 'female' else: gender = '' imported_patients.append({ 'row_num': row_num, 'file_number': file_number, 'patient_name': patient_name, 'first_name': first_name, 'last_name': last_name, 'mobile_no': mobile_no, 'ssn': ssn, 'gender': gender, 'visit_type': visit_type, 'admit_date': parsed_admit.isoformat() if parsed_admit else None, 'discharge_date': parsed_discharge.isoformat() if parsed_discharge else None, 'facility': facility, 'nationality': nationality, 'dob': parsed_dob.isoformat() if parsed_dob else None, }) 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), 'patients': imported_patients, 'errors': errors, 'total_count': len(imported_patients) } # Log audit AuditService.log_event( event_type='his_patient_import', description=f"Imported {len(imported_patients)} patients from HIS CSV by {user.get_full_name()}", user=user, metadata={ 'hospital': hospital.name, 'total_count': len(imported_patients), 'error_count': len(errors) } ) if imported_patients: messages.success( request, f"Successfully parsed {len(imported_patients)} patient records. Please review before creating." ) return redirect('surveys:his_patient_review') else: messages.error(request, "No valid patient records found in CSV.") except Exception as e: logger.error(f"Error processing HIS CSV: {str(e)}", exc_info=True) messages.error(request, f"Error processing CSV: {str(e)}") else: form = HISPatientImportForm(user=user) context = { 'form': form, } return render(request, 'surveys/his_patient_import.html', context) @login_required def his_patient_review(request): """ Review imported patients before creating records and sending surveys. """ user = request.user session_key = f'his_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('surveys:his_patient_import') hospital = get_object_or_404(Hospital, id=import_data['hospital_id']) patients = import_data['patients'] errors = import_data.get('errors', []) # Check for existing patients for p in patients: existing = Patient.objects.filter(mrn=p['file_number']).first() p['exists'] = existing is not None if request.method == 'POST': action = request.POST.get('action') selected_ids = request.POST.getlist('selected_patients') if action == 'create': # Create/update patient records created_count = 0 updated_count = 0 with transaction.atomic(): for p in patients: if p['file_number'] in selected_ids: patient, created = Patient.objects.update_or_create( mrn=p['file_number'], defaults={ 'first_name': p['first_name'], 'last_name': p['last_name'], 'phone': p['mobile_no'], 'national_id': p['ssn'], 'gender': p['gender'] if p['gender'] else '', 'primary_hospital': hospital, } ) if created: created_count += 1 else: updated_count += 1 # Store only the patient ID to avoid serialization issues p['patient_id'] = str(patient.id) # Update session with created patients request.session[session_key]['patients'] = patients request.session.modified = True messages.success( request, f"Created {created_count} new patients, updated {updated_count} existing patients." ) return redirect('surveys:his_patient_survey_send') elif action == 'cancel': del request.session[session_key] messages.info(request, "Import cancelled.") return redirect('surveys:his_patient_import') context = { 'hospital': hospital, 'patients': patients, 'errors': errors, 'total_count': len(patients), } return render(request, 'surveys/his_patient_review.html', context) @login_required def his_patient_survey_send(request): """ Send surveys to imported patients - Queues a background task. """ user = request.user session_key = f'his_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('surveys:his_patient_import') hospital = get_object_or_404(Hospital, id=import_data['hospital_id']) all_patients = import_data['patients'] # Filter only patients with records in database patients = [] for p in all_patients: if 'patient_id' in p: patients.append(p) if not patients: messages.error(request, "No patients have been created yet. Please create patients first.") return redirect('surveys:his_patient_review') if request.method == 'POST': form = HISSurveySendForm(request.POST, user=user) if form.is_valid(): survey_template = form.cleaned_data['survey_template'] delivery_channel = form.cleaned_data['delivery_channel'] custom_message = form.cleaned_data.get('custom_message', '') selected_ids = request.POST.getlist('selected_patients') # Filter selected patients selected_patients = [p for p in patients if p['file_number'] in selected_ids] if not selected_patients: messages.error(request, "No patients selected.") return render(request, 'surveys/his_patient_survey_send.html', { 'form': form, 'hospital': hospital, 'patients': patients, 'total_count': len(patients), }) # Create bulk job job = BulkSurveyJob.objects.create( name=f"HIS Import - {hospital.name} - {timezone.now().strftime('%Y-%m-%d %H:%M')}", status=BulkSurveyJob.JobStatus.PENDING, source=BulkSurveyJob.JobSource.HIS_IMPORT, created_by=user, hospital=hospital, survey_template=survey_template, total_patients=len(selected_patients), delivery_channel=delivery_channel, custom_message=custom_message, patient_data=selected_patients ) # Queue the background task send_bulk_surveys.delay(str(job.id)) # Log audit AuditService.log_event( event_type='his_survey_queued', description=f"Queued bulk survey job for {len(selected_patients)} patients", user=user, metadata={ 'job_id': str(job.id), 'hospital': hospital.name, 'survey_template': survey_template.name, 'patient_count': len(selected_patients) } ) # Clear session del request.session[session_key] messages.success( request, f"Survey sending job queued for {len(selected_patients)} patients. " f"You can check the status in Survey Jobs." ) return redirect('surveys:bulk_job_status', job_id=job.id) else: form = HISSurveySendForm(user=user) context = { 'form': form, 'hospital': hospital, 'patients': patients, 'total_count': len(patients), } return render(request, 'surveys/his_patient_survey_send.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 _parse_date(date_str): """Parse date from various formats""" if not date_str: return None formats = [ '%d-%b-%Y %H:%M:%S', '%d-%b-%Y', '%d/%m/%Y %H:%M:%S', '%d/%m/%Y', '%Y-%m-%d %H:%M:%S', '%Y-%m-%d', ] for fmt in formats: try: return datetime.strptime(date_str.strip(), fmt).date() except ValueError: continue return None @login_required def bulk_job_status(request, job_id): """ View status of a bulk survey job. """ user = request.user job = get_object_or_404(BulkSurveyJob, id=job_id) # Check permission if not user.is_px_admin() and job.created_by != user and job.hospital != user.hospital: messages.error(request, "You don't have permission to view this job.") return redirect('surveys:instance_list') context = { 'job': job, 'progress': job.progress_percentage, 'is_complete': job.is_complete, 'results': job.results, } return render(request, 'surveys/bulk_job_status.html', context) @login_required def bulk_job_list(request): """ List all bulk survey jobs for the user. """ user = request.user # Filter jobs if user.is_px_admin(): jobs = BulkSurveyJob.objects.all() elif user.hospital: jobs = BulkSurveyJob.objects.filter(hospital=user.hospital) else: jobs = BulkSurveyJob.objects.filter(created_by=user) jobs = jobs.order_by('-created_at')[:50] # Last 50 jobs context = { 'jobs': jobs, } return render(request, 'surveys/bulk_job_list.html', context)