HH/apps/feedback/models.py
2025-12-28 20:01:22 +03:00

362 lines
11 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'
)
physician = models.ForeignKey(
'organizations.Physician',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='feedbacks',
help_text="Physician 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)
# Metadata
source = models.CharField(
max_length=50,
default='web',
help_text="Source of feedback (web, mobile, kiosk, etc.)"
)
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')}"