""" Feedback models - Patient feedback and suggestions management This module implements the feedback management system that: - Tracks patient feedback (compliments, suggestions, general feedback) - Manages feedback workflow (submitted → reviewed → acknowledged → closed) - Maintains feedback responses and timeline - Supports attachments and ratings """ from django.conf import settings from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from apps.core.models import PriorityChoices, SoftDeleteModel, TimeStampedModel, UUIDModel class FeedbackType(models.TextChoices): """Feedback type choices""" COMPLIMENT = "compliment", _("Compliment") SUGGESTION = "suggestion", _("Suggestion") GENERAL = "general", _("General Feedback") INQUIRY = "inquiry", _("Inquiry") SATISFACTION_CHECK = "satisfaction_check", _("Satisfaction Check") class FeedbackStatus(models.TextChoices): """Feedback status choices""" SUBMITTED = "submitted", _("Submitted") REVIEWED = "reviewed", _("Reviewed") ACKNOWLEDGED = "acknowledged", _("Acknowledged") CLOSED = "closed", _("Closed") class FeedbackCategory(models.TextChoices): """Feedback category choices""" CLINICAL_CARE = "clinical_care", _("Clinical Care") STAFF_SERVICE = "staff_service", _("Staff Service") FACILITY = "facility", _("Facility & Environment") COMMUNICATION = "communication", _("Communication") APPOINTMENT = "appointment", _("Appointment & Scheduling") BILLING = "billing", _("Billing & Insurance") FOOD_SERVICE = "food_service", _("Food Service") CLEANLINESS = "cleanliness", _("Cleanliness") TECHNOLOGY = "technology", _("Technology & Systems") OTHER = "other", _("Other") class SentimentChoices(models.TextChoices): """Sentiment analysis choices""" POSITIVE = "positive", _("Positive") NEUTRAL = "neutral", _("Neutral") NEGATIVE = "negative", _("Negative") class Feedback(UUIDModel, TimeStampedModel, SoftDeleteModel): """ Feedback model for patient feedback, compliments, and suggestions. Workflow: 1. SUBMITTED - Feedback received 2. REVIEWED - Being reviewed by staff 3. ACKNOWLEDGED - Response provided 4. CLOSED - Feedback closed """ # Patient and encounter information patient = models.ForeignKey( "organizations.Patient", on_delete=models.CASCADE, related_name="feedbacks", null=True, blank=True, help_text="Patient who provided feedback (optional for anonymous feedback)", ) # Anonymous feedback support is_anonymous = models.BooleanField(default=False) contact_name = models.CharField(max_length=200, blank=True) contact_email = models.EmailField(blank=True) contact_phone = models.CharField(max_length=20, blank=True) encounter_id = models.CharField( max_length=100, blank=True, db_index=True, help_text="Related encounter ID if applicable" ) # Survey linkage (for satisfaction checks after negative surveys) related_survey = models.ForeignKey( "surveys.SurveyInstance", on_delete=models.SET_NULL, null=True, blank=True, related_name="follow_up_feedbacks", help_text="Survey that triggered this satisfaction check feedback", ) # Organization hospital = models.ForeignKey("organizations.Hospital", on_delete=models.CASCADE, related_name="feedbacks") location = models.ForeignKey( "organizations.Location", on_delete=models.SET_NULL, null=True, blank=True, related_name="feedbacks", help_text="Location context", ) main_section = models.ForeignKey( "organizations.MainSection", on_delete=models.SET_NULL, null=True, blank=True, related_name="feedbacks", help_text="Main section within the location", ) subsection = models.ForeignKey( "organizations.SubSection", on_delete=models.SET_NULL, null=True, blank=True, related_name="feedbacks", help_text="Specific subsection", ) department = models.ForeignKey( "organizations.Department", on_delete=models.SET_NULL, null=True, blank=True, related_name="feedbacks" ) staff = models.ForeignKey( "organizations.Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="feedbacks", help_text="Staff member being mentioned in feedback", ) # Feedback details feedback_type = models.CharField( max_length=20, choices=FeedbackType.choices, default=FeedbackType.GENERAL, db_index=True ) title = models.CharField(max_length=500) message = models.TextField(help_text="Feedback message") # Classification category = models.CharField(max_length=50, choices=FeedbackCategory.choices, db_index=True) subcategory = models.CharField(max_length=100, blank=True) # Rating (1-5 stars) rating = models.IntegerField(null=True, blank=True, help_text="Rating from 1 to 5 stars") # Priority priority = models.CharField( max_length=20, choices=PriorityChoices.choices, default=PriorityChoices.MEDIUM, db_index=True ) # Sentiment analysis sentiment = models.CharField( max_length=20, choices=SentimentChoices.choices, default=SentimentChoices.NEUTRAL, db_index=True, help_text="Sentiment analysis result", ) sentiment_score = models.FloatField( null=True, blank=True, help_text="Sentiment score from -1 (negative) to 1 (positive)" ) # Status and workflow status = models.CharField( max_length=20, choices=FeedbackStatus.choices, default=FeedbackStatus.SUBMITTED, db_index=True ) # Assignment assigned_to = models.ForeignKey( "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_feedbacks" ) assigned_at = models.DateTimeField(null=True, blank=True) # Review tracking reviewed_at = models.DateTimeField(null=True, blank=True) reviewed_by = models.ForeignKey( "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="reviewed_feedbacks" ) # Acknowledgment acknowledged_at = models.DateTimeField(null=True, blank=True) acknowledged_by = models.ForeignKey( "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="acknowledged_feedbacks" ) # Closure closed_at = models.DateTimeField(null=True, blank=True) closed_by = models.ForeignKey( "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="closed_feedbacks" ) # Flags is_featured = models.BooleanField(default=False, help_text="Feature this feedback (e.g., for testimonials)") is_public = models.BooleanField(default=False, help_text="Make this feedback public") requires_follow_up = models.BooleanField(default=False) # Source source = models.ForeignKey( "px_sources.PXSource", on_delete=models.PROTECT, related_name="feedbacks", null=True, blank=True, help_text="Source of feedback", ) # Metadata metadata = models.JSONField(default=dict, blank=True) class Meta: ordering = ["-created_at"] indexes = [ models.Index(fields=["status", "-created_at"]), models.Index(fields=["hospital", "status", "-created_at"]), models.Index(fields=["department", "status", "-created_at"]), models.Index(fields=["assigned_to", "status", "-created_at"]), models.Index(fields=["feedback_type", "-created_at"]), models.Index(fields=["is_deleted", "-created_at"]), ] verbose_name_plural = "Feedback" def __str__(self): if self.patient: return f"{self.title} - {self.patient.get_full_name()} ({self.feedback_type})" return f"{self.title} - Anonymous ({self.feedback_type})" def get_absolute_url(self): from django.urls import reverse return reverse("feedback:feedback_detail", kwargs={"pk": self.pk}) @property def has_ai_analysis(self): return bool(self.metadata and self.metadata.get("ai_analysis")) @property def ai_analysis(self): return self.metadata.get("ai_analysis", {}) if self.metadata else {} @property def ai_short_description_en(self): return self.ai_analysis.get("short_description_en", "") @property def ai_short_description_ar(self): return self.ai_analysis.get("short_description_ar", "") @property def ai_suggested_actions(self): return self.ai_analysis.get("suggested_actions", []) @property def ai_suggested_action_en(self): return self.ai_analysis.get("suggested_action_en", "") @property def ai_suggested_action_ar(self): return self.ai_analysis.get("suggested_action_ar", "") @property def ai_priority(self): return self.ai_analysis.get("priority", "") @property def ai_category(self): return self.ai_analysis.get("category", "") @property def ai_reasoning_en(self): return self.ai_analysis.get("reasoning_en", "") @property def ai_reasoning_ar(self): return self.ai_analysis.get("reasoning_ar", "") def get_contact_name(self): """Get contact name (patient or anonymous)""" if self.patient: return self.patient.get_full_name() return self.contact_name or "Anonymous" class FeedbackAttachment(UUIDModel, TimeStampedModel): """Feedback attachment (images, documents, etc.)""" feedback = models.ForeignKey(Feedback, on_delete=models.CASCADE, related_name="attachments") file = models.FileField(upload_to="feedback/%Y/%m/%d/") filename = models.CharField(max_length=500) file_type = models.CharField(max_length=100, blank=True) file_size = models.IntegerField(help_text="File size in bytes") uploaded_by = models.ForeignKey( "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="feedback_attachments" ) description = models.TextField(blank=True) class Meta: ordering = ["-created_at"] def __str__(self): return f"{self.feedback} - {self.filename}" class FeedbackResponse(UUIDModel, TimeStampedModel): """ Feedback response/timeline entry. Tracks all responses, status changes, and communications. """ feedback = models.ForeignKey(Feedback, on_delete=models.CASCADE, related_name="responses") # Response details response_type = models.CharField( max_length=50, choices=[ ("status_change", "Status Change"), ("assignment", "Assignment"), ("note", "Internal Note"), ("response", "Response to Patient"), ("acknowledgment", "Acknowledgment"), ], db_index=True, ) message = models.TextField() # User who made the response created_by = models.ForeignKey( "accounts.User", on_delete=models.SET_NULL, null=True, related_name="feedback_responses" ) # Status change tracking old_status = models.CharField(max_length=20, blank=True) new_status = models.CharField(max_length=20, blank=True) # Visibility is_internal = models.BooleanField(default=False, help_text="Internal note (not visible to patient)") # Metadata metadata = models.JSONField(default=dict, blank=True) class Meta: ordering = ["-created_at"] indexes = [ models.Index(fields=["feedback", "-created_at"]), ] def __str__(self): return f"{self.feedback} - {self.response_type} - {self.created_at.strftime('%Y-%m-%d %H:%M')}" class CommentSourceCategory(models.TextChoices): """Source category from IT department data export (Step 0)""" APPOINTMENT = "appointment", _("Appointment") INPATIENT = "inpatient", _("Inpatient") OUTPATIENT = "outpatient", _("Outpatient") class CommentClassification(models.TextChoices): """Comment classification categories (Step 1 — Colors sheet)""" HOSPITAL = "hospital", _("Hospital") MEDICAL = "medical", _("Medical") NON_MEDICAL = "non_medical", _("Non-Medical") NURSING = "nursing", _("Nursing") ER = "er", _("Emergency") SUPPORT_SERVICES = "support_services", _("Support Services") class CommentSubCategory(models.TextChoices): """Comment sub-categories (Step 1 — Colors sheet dropdown)""" PHARMACY = "pharmacy", _("Pharmacy") RAD = "rad", _("RAD") LAB = "lab", _("LAB") PHYSIOTHERAPY = "physiotherapy", _("Physiotherapy") DOCTORS = "doctors", _("Doctors") MEDICAL_REPORTS = "medical_reports", _("Medical Reports") RECEPTION = "reception", _("Reception") INSURANCE_APPROVALS = "insurance_approvals", _("Insurance/Approvals") OPD_CLINICS = "opd_clinics", _("OPD - Clinics") APPOINTMENTS = "appointments", _("Appointments") IT_APP = "it_app", _("IT - App") ADMINISTRATION = "administration", _("Administration") BILLING = "billing", _("Billing") FACILITIES = "facilities", _("Facilities") FOOD_SERVICES = "food_services", _("Food Services") PARKING = "parking", _("Parking") HOUSEKEEPING = "housekeeping", _("Housekeeping") OTHER = "other", _("Other") COMMENT_SUB_CATEGORY_MAP = { CommentClassification.MEDICAL: [ CommentSubCategory.PHARMACY, CommentSubCategory.RAD, CommentSubCategory.LAB, CommentSubCategory.PHYSIOTHERAPY, CommentSubCategory.DOCTORS, CommentSubCategory.MEDICAL_REPORTS, ], CommentClassification.NON_MEDICAL: [ CommentSubCategory.RECEPTION, CommentSubCategory.INSURANCE_APPROVALS, CommentSubCategory.OPD_CLINICS, CommentSubCategory.APPOINTMENTS, CommentSubCategory.IT_APP, CommentSubCategory.ADMINISTRATION, CommentSubCategory.BILLING, ], CommentClassification.NURSING: [], CommentClassification.ER: [ CommentSubCategory.DOCTORS, CommentSubCategory.RECEPTION, ], CommentClassification.SUPPORT_SERVICES: [ CommentSubCategory.FACILITIES, CommentSubCategory.FOOD_SERVICES, CommentSubCategory.PARKING, CommentSubCategory.HOUSEKEEPING, ], CommentClassification.HOSPITAL: [], } class CommentImport(UUIDModel, TimeStampedModel): """ Tracks IT department comment data imports (Step 0). Each import represents a monthly batch of raw patient comments exported from the IT system. """ IMPORT_STATUS_CHOICES = [ ("pending", _("Pending")), ("processing", _("Processing")), ("completed", _("Completed")), ("failed", _("Failed")), ] hospital = models.ForeignKey( "organizations.Hospital", on_delete=models.CASCADE, related_name="comment_imports", ) month = models.IntegerField(help_text="Month number (1-12)") year = models.IntegerField(help_text="Year") source_file = models.FileField( upload_to="comments/imports/%Y/%m/", blank=True, help_text="Uploaded source file from IT department", ) status = models.CharField(max_length=20, choices=IMPORT_STATUS_CHOICES, default="pending") total_rows = models.IntegerField(default=0, help_text="Total rows in the import file") imported_count = models.IntegerField(default=0, help_text="Number of comments successfully imported") error_count = models.IntegerField(default=0) error_log = models.TextField(blank=True) imported_by = models.ForeignKey( "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="comment_imports", ) class Meta: ordering = ["-year", "-month"] unique_together = [["hospital", "year", "month"]] verbose_name = "Comment Import" verbose_name_plural = "Comment Imports" def __str__(self): return f"{self.hospital} - {self.year}-{self.month:02d} ({self.imported_count} comments)" class PatientComment(UUIDModel, TimeStampedModel): """ Patient comment from IT department data export. Workflow (Steps 0-5): - Step 0: Raw import from IT (source_category, comment_text) - Step 1: Classification (classification, sub_category, sentiment keywords) - Step 2: Department filtering (grouped by department for review) - Step 3: Action plan creation (linked action plans) - Step 5: Action plan tracking (status, timeframe, evidence) One comment can have multiple classifications (multi-category). Use PatientCommentClassification for multi-category support. """ comment_import = models.ForeignKey( CommentImport, on_delete=models.CASCADE, related_name="comments", null=True, blank=True, help_text="Import batch this comment came from", ) hospital = models.ForeignKey( "organizations.Hospital", on_delete=models.CASCADE, related_name="patient_comments", ) serial_number = models.IntegerField(null=True, blank=True, help_text="Serial number from the source file") source_category = models.CharField( max_length=20, choices=CommentSourceCategory.choices, blank=True, db_index=True, help_text="Source category from IT export (Appointment/Inpatient/Outpatient)", ) comment_text = models.TextField(help_text="Original comment text (Arabic/English)") comment_text_en = models.TextField(blank=True, help_text="English translation of the comment") classification = models.CharField( max_length=20, choices=CommentClassification.choices, blank=True, db_index=True, help_text="Primary classification (Hospital/Medical/Non-Medical/Nursing/ER/Support)", ) sub_category = models.CharField( max_length=30, choices=CommentSubCategory.choices, blank=True, db_index=True, help_text="Sub-category classification (e.g., Pharmacy, Reception, etc.)", ) negative_keywords = models.TextField(blank=True, help_text="Negative sentiment keywords/phrases extracted") positive_keywords = models.TextField(blank=True, help_text="Positive sentiment keywords/phrases extracted") gratitude_keywords = models.TextField(blank=True, help_text="Gratitude keywords/phrases extracted") suggestions = models.TextField(blank=True, help_text="Suggestion text extracted from the comment") sentiment = models.CharField( max_length=20, choices=SentimentChoices.choices, blank=True, db_index=True, help_text="Overall sentiment classification", ) is_classified = models.BooleanField( default=False, db_index=True, help_text="Whether this comment has been classified", ) mentioned_doctor_name = models.CharField( max_length=200, blank=True, help_text="Name of doctor mentioned in the comment", ) mentioned_doctor_name_en = models.CharField( max_length=200, blank=True, help_text="English name of mentioned doctor", ) frequency = models.IntegerField( default=1, help_text="How many times this comment/problem has been reported", ) month = models.IntegerField(null=True, blank=True, help_text="Month the comment was collected") year = models.IntegerField(null=True, blank=True, help_text="Year the comment was collected") metadata = models.JSONField(default=dict, blank=True) class Meta: ordering = ["-year", "-month", "-serial_number"] indexes = [ models.Index(fields=["hospital", "classification", "sub_category"]), models.Index(fields=["hospital", "year", "month"]), models.Index(fields=["hospital", "source_category"]), ] verbose_name = "Patient Comment" verbose_name_plural = "Patient Comments" def __str__(self): text = self.comment_text[:50] if self.comment_text else "No text" return f"#{self.serial_number or '—'} {text}" class CommentActionPlanStatus(models.TextChoices): COMPLETED = "completed", _("Completed") ON_PROCESS = "on_process", _("On Process") PENDING = "pending", _("Pending") class CommentActionPlan(UUIDModel, TimeStampedModel): """ Action plan derived from classified patient comments (Step 3/5). Tracks recommendations/action plans with responsible departments, timeframes, and completion status. """ comment = models.ForeignKey( PatientComment, on_delete=models.CASCADE, related_name="action_plans", null=True, blank=True, help_text="Source comment (may be null if aggregated from multiple)", ) hospital = models.ForeignKey( "organizations.Hospital", on_delete=models.CASCADE, related_name="comment_action_plans", ) department = models.ForeignKey( "organizations.Department", on_delete=models.SET_NULL, null=True, blank=True, related_name="comment_action_plans", help_text="Responsible department", ) department_label = models.CharField( max_length=200, blank=True, help_text="Free-text department name (for when no FK exists)", ) problem_number = models.IntegerField(null=True, blank=True, help_text="Problem number within the department") comment_text = models.TextField(blank=True, help_text="Related comment text") comment_text_en = models.TextField(blank=True, help_text="English translation of the comment") frequency = models.IntegerField(default=1, help_text="Number of times this problem was reported") recommendation = models.TextField(help_text="Recommendation / Action Plan") recommendation_en = models.TextField(blank=True, help_text="English translation of the recommendation") responsible_department = models.CharField( max_length=200, blank=True, help_text="Free-text responsible department name", ) status = models.CharField( max_length=20, choices=CommentActionPlanStatus.choices, default=CommentActionPlanStatus.PENDING, db_index=True, ) timeframe = models.CharField( max_length=100, blank=True, help_text="Target timeframe (e.g., Q3, 3 months, 2025-Q1)", ) evidences = models.TextField(blank=True, help_text="Evidence of completion / notes") month = models.IntegerField(null=True, blank=True) year = models.IntegerField(null=True, blank=True) class Meta: ordering = ["department_label", "problem_number"] indexes = [ models.Index(fields=["hospital", "status"]), models.Index(fields=["hospital", "year", "month"]), ] verbose_name = "Comment Action Plan" verbose_name_plural = "Comment Action Plans" def __str__(self): dept = self.department_label or (self.department.name if self.department else "—") return f"{dept} - Problem #{self.problem_number or '—'} ({self.get_status_display()})"