""" 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 (optional - HIS ratings don't include patient data) patient_uhid = models.CharField( max_length=100, blank=True, null=True, db_index=True, help_text="Patient UHID/MRN (optional for HIS ratings)" ) patient_name = models.CharField(max_length=300, blank=True, null=True) patient_gender = models.CharField(max_length=20, blank=True, default="") patient_age = models.CharField(max_length=50, blank=True, default="") patient_nationality = models.CharField(max_length=100, blank=True, default="") patient_phone = models.CharField(max_length=30, blank=True, default="") patient_type = models.CharField(max_length=10, choices=PatientType.choices, blank=True, null=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