645 lines
21 KiB
Python
645 lines
21 KiB
Python
"""
|
|
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 apps.core.models import PriorityChoices, 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):
|
|
"""
|
|
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")
|
|
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)
|
|
|
|
# Soft delete
|
|
is_deleted = models.BooleanField(default=False, db_index=True)
|
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
|
deleted_by = models.ForeignKey(
|
|
"accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="deleted_feedbacks"
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ["-created_at"]
|
|
indexes = [
|
|
models.Index(fields=["status", "-created_at"]),
|
|
models.Index(fields=["hospital", "status", "-created_at"]),
|
|
models.Index(fields=["feedback_type", "-created_at"]),
|
|
models.Index(fields=["sentiment", "-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_contact_name(self):
|
|
"""Get contact name (patient or anonymous)"""
|
|
if self.patient:
|
|
return self.patient.get_full_name()
|
|
return self.contact_name or "Anonymous"
|
|
|
|
def soft_delete(self, user=None):
|
|
"""Soft delete feedback"""
|
|
self.is_deleted = True
|
|
self.deleted_at = timezone.now()
|
|
self.deleted_by = user
|
|
self.save(update_fields=["is_deleted", "deleted_at", "deleted_by"])
|
|
|
|
|
|
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", "ER"
|
|
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"]),
|
|
models.Index(fields=["sentiment"]),
|
|
]
|
|
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()})"
|