362 lines
10 KiB
Python
362 lines
10 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)
|
|
|
|
# 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')}"
|