""" Physicians models - Physician ratings and performance tracking This module implements physician performance tracking: - Monthly rating aggregation from surveys - Performance metrics - Leaderboards - HIS Doctor Rating imports """ from django.db import models from apps.core.models import TimeStampedModel, UUIDModel class PhysicianMonthlyRating(UUIDModel, TimeStampedModel): """ Physician monthly rating - aggregated from survey responses. Calculated monthly from all surveys mentioning this physician. """ staff = models.ForeignKey( 'organizations.Staff', on_delete=models.CASCADE, related_name='monthly_ratings' ) # Time period year = models.IntegerField(db_index=True) month = models.IntegerField(db_index=True, help_text="1-12") # Ratings average_rating = models.DecimalField( max_digits=3, decimal_places=2, help_text="Average rating (1-5)" ) total_surveys = models.IntegerField( help_text="Number of surveys included" ) positive_count = models.IntegerField(default=0) neutral_count = models.IntegerField(default=0) negative_count = models.IntegerField(default=0) # Breakdown by journey stage md_consult_rating = models.DecimalField( max_digits=3, decimal_places=2, null=True, blank=True ) # Ranking hospital_rank = models.IntegerField( null=True, blank=True, help_text="Rank within hospital" ) department_rank = models.IntegerField( null=True, blank=True, help_text="Rank within department" ) # Metadata metadata = models.JSONField(default=dict, blank=True) class Meta: ordering = ['-year', '-month', '-average_rating'] unique_together = [['staff', 'year', 'month']] indexes = [ models.Index(fields=['staff', '-year', '-month']), models.Index(fields=['year', 'month', '-average_rating']), ] def __str__(self): return f"{self.staff.get_full_name()} - {self.year}-{self.month:02d}: {self.average_rating}" class PhysicianIndividualRating(UUIDModel, TimeStampedModel): """ Individual physician rating from HIS or manual import. Stores each individual patient rating before aggregation. Source can be HIS integration, CSV import, or manual entry. """ class RatingSource(models.TextChoices): HIS_API = 'his_api', 'HIS API' CSV_IMPORT = 'csv_import', 'CSV Import' MANUAL = 'manual', 'Manual Entry' class PatientType(models.TextChoices): INPATIENT = 'IP', 'Inpatient' OUTPATIENT = 'OP', 'Outpatient' EMERGENCY = 'ER', 'Emergency' DAYCASE = 'DC', 'Day Case' # Links staff = models.ForeignKey( 'organizations.Staff', on_delete=models.CASCADE, related_name='individual_ratings', null=True, blank=True, help_text="Linked staff record (if matched)" ) hospital = models.ForeignKey( 'organizations.Hospital', on_delete=models.CASCADE, related_name='physician_ratings' ) # Source tracking source = models.CharField( max_length=20, choices=RatingSource.choices, default=RatingSource.MANUAL ) source_reference = models.CharField( max_length=100, blank=True, help_text="Reference ID from source system (e.g., HIS record ID)" ) # Doctor information (as received from source) doctor_name_raw = models.CharField( max_length=300, help_text="Doctor name as received (may include ID prefix)" ) doctor_id = models.CharField( max_length=50, blank=True, db_index=True, help_text="Doctor ID extracted from source (e.g., '10738')" ) doctor_name = models.CharField( max_length=200, blank=True, help_text="Clean doctor name without ID" ) department_name = models.CharField( max_length=200, blank=True, help_text="Department name from source" ) # Patient information patient_uhid = models.CharField( max_length=100, db_index=True, help_text="Patient UHID/MRN" ) patient_name = models.CharField(max_length=300) patient_gender = models.CharField(max_length=20, blank=True) patient_age = models.CharField(max_length=50, blank=True) patient_nationality = models.CharField(max_length=100, blank=True) patient_phone = models.CharField(max_length=30, blank=True) patient_type = models.CharField( max_length=10, choices=PatientType.choices, blank=True ) # Visit dates admit_date = models.DateTimeField(null=True, blank=True) discharge_date = models.DateTimeField(null=True, blank=True) # Rating data rating = models.IntegerField( help_text="Rating from 1-5" ) feedback = models.TextField(blank=True) rating_date = models.DateTimeField() # Aggregation tracking is_aggregated = models.BooleanField( default=False, help_text="Whether this rating has been included in monthly aggregation" ) aggregated_at = models.DateTimeField(null=True, blank=True) # Metadata metadata = models.JSONField( default=dict, blank=True, help_text="Additional data from source" ) class Meta: ordering = ['-rating_date', '-created_at'] indexes = [ models.Index(fields=['hospital', '-rating_date']), models.Index(fields=['staff', '-rating_date']), models.Index(fields=['doctor_id', '-rating_date']), models.Index(fields=['is_aggregated', 'rating_date']), models.Index(fields=['patient_uhid', '-rating_date']), ] def __str__(self): return f"{self.doctor_name or self.doctor_name_raw} - {self.rating}/5 on {self.rating_date.date()}" class DoctorRatingImportJob(UUIDModel, TimeStampedModel): """ Tracks bulk doctor rating import jobs (CSV or API batch). """ class JobStatus(models.TextChoices): PENDING = 'pending', 'Pending' PROCESSING = 'processing', 'Processing' COMPLETED = 'completed', 'Completed' FAILED = 'failed', 'Failed' PARTIAL = 'partial', 'Partial Success' class JobSource(models.TextChoices): HIS_API = 'his_api', 'HIS API' CSV_UPLOAD = 'csv_upload', 'CSV Upload' # Job info name = models.CharField(max_length=200) status = models.CharField( max_length=20, choices=JobStatus.choices, default=JobStatus.PENDING ) source = models.CharField( max_length=20, choices=JobSource.choices ) # User & Organization created_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, related_name='doctor_rating_jobs' ) hospital = models.ForeignKey( 'organizations.Hospital', on_delete=models.CASCADE, related_name='doctor_rating_jobs' ) # Progress tracking total_records = models.IntegerField(default=0) processed_count = models.IntegerField(default=0) success_count = models.IntegerField(default=0) failed_count = models.IntegerField(default=0) skipped_count = models.IntegerField(default=0) # Timing started_at = models.DateTimeField(null=True, blank=True) completed_at = models.DateTimeField(null=True, blank=True) # Results results = models.JSONField( default=dict, blank=True, help_text="Processing results and errors" ) error_message = models.TextField(blank=True) # Raw data storage (for CSV uploads) raw_data = models.JSONField( default=list, blank=True, help_text="Stored raw data for processing" ) class Meta: ordering = ['-created_at'] def __str__(self): return f"{self.name} - {self.status}" @property def progress_percentage(self): if self.total_records == 0: return 0 return int((self.processed_count / self.total_records) * 100) @property def is_complete(self): return self.status in [self.JobStatus.COMPLETED, self.JobStatus.FAILED, self.JobStatus.PARTIAL] @property def duration_seconds(self): if self.started_at and self.completed_at: return (self.completed_at - self.started_at).total_seconds() return None