467 lines
14 KiB
Python
467 lines
14 KiB
Python
"""
|
|
Appreciation models - Send and track appreciation to users and physicians
|
|
|
|
This module implements the appreciation system that:
|
|
- Allows sending appreciation messages to users and physicians
|
|
- Tracks appreciation statistics and leaderboards
|
|
- Manages appreciation categories and badges
|
|
- Integrates with the notification system
|
|
"""
|
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.db import models
|
|
|
|
from apps.core.models import TimeStampedModel, UUIDModel
|
|
|
|
|
|
class AppreciationStatus(models.TextChoices):
|
|
"""Appreciation status choices"""
|
|
DRAFT = 'draft', 'Draft'
|
|
SENT = 'sent', 'Sent'
|
|
ACKNOWLEDGED = 'acknowledged', 'Acknowledged'
|
|
|
|
|
|
class AppreciationVisibility(models.TextChoices):
|
|
"""Appreciation visibility choices"""
|
|
PRIVATE = 'private', 'Private'
|
|
DEPARTMENT = 'department', 'Department'
|
|
HOSPITAL = 'hospital', 'Hospital'
|
|
PUBLIC = 'public', 'Public'
|
|
|
|
|
|
class AppreciationCategory(UUIDModel, TimeStampedModel):
|
|
"""
|
|
Appreciation category with bilingual support.
|
|
|
|
Pre-defined categories for sending appreciation (e.g., "Excellent Care", "Team Player").
|
|
Can be hospital-specific or system-wide.
|
|
"""
|
|
# Organization
|
|
hospital = models.ForeignKey(
|
|
'organizations.Hospital',
|
|
on_delete=models.CASCADE,
|
|
related_name='appreciation_categories',
|
|
null=True,
|
|
blank=True,
|
|
help_text="Leave blank for system-wide categories"
|
|
)
|
|
|
|
# Category details
|
|
code = models.CharField(
|
|
max_length=50,
|
|
help_text="Unique code for this category"
|
|
)
|
|
name_en = models.CharField(max_length=200)
|
|
name_ar = models.CharField(max_length=200, blank=True)
|
|
|
|
description_en = models.TextField(blank=True)
|
|
description_ar = models.TextField(blank=True)
|
|
|
|
# Visual elements
|
|
icon = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
help_text="Icon class (e.g., 'fa-heart', 'fa-star')"
|
|
)
|
|
color = models.CharField(
|
|
max_length=7,
|
|
blank=True,
|
|
help_text="Hex color code (e.g., '#FF5733')"
|
|
)
|
|
|
|
# Display order
|
|
order = models.IntegerField(default=0, help_text="Display order")
|
|
|
|
# Status
|
|
is_active = models.BooleanField(default=True)
|
|
|
|
class Meta:
|
|
ordering = ['hospital', 'order', 'name_en']
|
|
unique_together = [['hospital', 'code']]
|
|
verbose_name_plural = 'Appreciation Categories'
|
|
indexes = [
|
|
models.Index(fields=['hospital', 'is_active']),
|
|
models.Index(fields=['code']),
|
|
]
|
|
|
|
def __str__(self):
|
|
hospital_name = self.hospital.name if self.hospital else "System-wide"
|
|
return f"{hospital_name} - {self.name_en}"
|
|
|
|
|
|
class Appreciation(UUIDModel, TimeStampedModel):
|
|
"""
|
|
Appreciation model for sending appreciation to users and physicians.
|
|
|
|
Workflow:
|
|
1. DRAFT - Created but not yet sent
|
|
2. SENT - Sent to recipient (triggers notification)
|
|
3. ACKNOWLEDGED - Recipient has acknowledged/thanked sender
|
|
|
|
Visibility:
|
|
- PRIVATE - Only sender and recipient can view
|
|
- DEPARTMENT - Visible to department members
|
|
- HOSPITAL - Visible to all hospital staff
|
|
- PUBLIC - Can be displayed publicly (e.g., on appreciation wall)
|
|
"""
|
|
# Sender
|
|
sender = models.ForeignKey(
|
|
'accounts.User',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='sent_appreciations'
|
|
)
|
|
|
|
# Recipient (Generic FK to User or Physician)
|
|
recipient_content_type = models.ForeignKey(
|
|
ContentType,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='appreciation_recipients'
|
|
)
|
|
recipient_object_id = models.UUIDField(null=True, blank=True)
|
|
recipient = GenericForeignKey(
|
|
'recipient_content_type',
|
|
'recipient_object_id'
|
|
)
|
|
|
|
# Organization context
|
|
hospital = models.ForeignKey(
|
|
'organizations.Hospital',
|
|
on_delete=models.CASCADE,
|
|
related_name='appreciations'
|
|
)
|
|
department = models.ForeignKey(
|
|
'organizations.Department',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='appreciations',
|
|
help_text="Department context (if applicable)"
|
|
)
|
|
|
|
# Category and message
|
|
category = models.ForeignKey(
|
|
AppreciationCategory,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='appreciations'
|
|
)
|
|
message_en = models.TextField()
|
|
message_ar = models.TextField(blank=True)
|
|
|
|
# Visibility and status
|
|
visibility = models.CharField(
|
|
max_length=20,
|
|
choices=AppreciationVisibility.choices,
|
|
default=AppreciationVisibility.PRIVATE,
|
|
db_index=True
|
|
)
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=AppreciationStatus.choices,
|
|
default=AppreciationStatus.DRAFT,
|
|
db_index=True
|
|
)
|
|
|
|
# Anonymous option
|
|
is_anonymous = models.BooleanField(
|
|
default=False,
|
|
help_text="Hide sender identity from recipient"
|
|
)
|
|
|
|
# Timestamps
|
|
sent_at = models.DateTimeField(null=True, blank=True)
|
|
acknowledged_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
# Notification tracking
|
|
notification_sent = models.BooleanField(default=False)
|
|
notification_sent_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
# 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']),
|
|
models.Index(fields=['recipient_content_type', 'recipient_object_id', '-created_at']),
|
|
models.Index(fields=['visibility', '-created_at']),
|
|
]
|
|
|
|
def __str__(self):
|
|
recipient_name = self.get_recipient_name()
|
|
return f"Appreciation to {recipient_name} ({self.status})"
|
|
|
|
def get_recipient_name(self):
|
|
"""Get recipient's name"""
|
|
try:
|
|
return str(self.recipient)
|
|
except:
|
|
return "Unknown"
|
|
|
|
def get_recipient_email(self):
|
|
"""Get recipient's email"""
|
|
try:
|
|
if hasattr(self.recipient, 'email'):
|
|
return self.recipient.email
|
|
elif hasattr(self.recipient, 'user'):
|
|
return self.recipient.user.email
|
|
except:
|
|
pass
|
|
return None
|
|
|
|
def get_recipient_phone(self):
|
|
"""Get recipient's phone"""
|
|
try:
|
|
if hasattr(self.recipient, 'phone'):
|
|
return self.recipient.phone
|
|
elif hasattr(self.recipient, 'user'):
|
|
return self.recipient.user.phone
|
|
except:
|
|
pass
|
|
return None
|
|
|
|
def send(self):
|
|
"""Send appreciation and trigger notification"""
|
|
from django.utils import timezone
|
|
|
|
self.status = AppreciationStatus.SENT
|
|
self.sent_at = timezone.now()
|
|
self.save(update_fields=['status', 'sent_at'])
|
|
|
|
# Trigger notification
|
|
self.send_notification()
|
|
|
|
def acknowledge(self):
|
|
"""Mark appreciation as acknowledged"""
|
|
from django.utils import timezone
|
|
|
|
self.status = AppreciationStatus.ACKNOWLEDGED
|
|
self.acknowledged_at = timezone.now()
|
|
self.save(update_fields=['status', 'acknowledged_at'])
|
|
|
|
def send_notification(self):
|
|
"""Send notification to recipient"""
|
|
# This will be implemented in signals.py
|
|
pass
|
|
|
|
|
|
class AppreciationBadge(UUIDModel, TimeStampedModel):
|
|
"""
|
|
Appreciation badge for gamification.
|
|
|
|
Badges are awarded based on achievements (e.g., "10 Appreciations Received").
|
|
"""
|
|
# Organization
|
|
hospital = models.ForeignKey(
|
|
'organizations.Hospital',
|
|
on_delete=models.CASCADE,
|
|
related_name='appreciation_badges',
|
|
null=True,
|
|
blank=True,
|
|
help_text="Leave blank for system-wide badges"
|
|
)
|
|
|
|
# Badge details
|
|
code = models.CharField(
|
|
max_length=50,
|
|
unique=True,
|
|
help_text="Unique badge code"
|
|
)
|
|
name_en = models.CharField(max_length=200)
|
|
name_ar = models.CharField(max_length=200, blank=True)
|
|
description_en = models.TextField(blank=True)
|
|
description_ar = models.TextField(blank=True)
|
|
|
|
# Visual elements
|
|
icon = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
help_text="Icon class"
|
|
)
|
|
color = models.CharField(
|
|
max_length=7,
|
|
blank=True,
|
|
help_text="Hex color code"
|
|
)
|
|
|
|
# Achievement criteria
|
|
criteria_type = models.CharField(
|
|
max_length=50,
|
|
choices=[
|
|
('received_count', 'Total Appreciations Received'),
|
|
('received_month', 'Appreciations Received in a Month'),
|
|
('streak_weeks', 'Consecutive Weeks with Appreciation'),
|
|
('diverse_senders', 'Appreciations from Different Senders'),
|
|
],
|
|
db_index=True
|
|
)
|
|
criteria_value = models.IntegerField(
|
|
help_text="Value to achieve (e.g., 10 for 10 appreciations)"
|
|
)
|
|
|
|
# Display order
|
|
order = models.IntegerField(default=0)
|
|
|
|
# Status
|
|
is_active = models.BooleanField(default=True)
|
|
|
|
class Meta:
|
|
ordering = ['hospital', 'order', 'name_en']
|
|
indexes = [
|
|
models.Index(fields=['hospital', 'is_active']),
|
|
models.Index(fields=['code']),
|
|
]
|
|
|
|
def __str__(self):
|
|
hospital_name = self.hospital.name if self.hospital else "System-wide"
|
|
return f"{hospital_name} - {self.name_en}"
|
|
|
|
|
|
class UserBadge(UUIDModel, TimeStampedModel):
|
|
"""
|
|
User badge - tracks badges earned by users.
|
|
|
|
Links users (or physicians) to badges they've earned.
|
|
"""
|
|
# Recipient (Generic FK to User or Physician)
|
|
recipient_content_type = models.ForeignKey(
|
|
ContentType,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='earned_badges_recipients'
|
|
)
|
|
recipient_object_id = models.UUIDField(null=True, blank=True)
|
|
recipient = GenericForeignKey(
|
|
'recipient_content_type',
|
|
'recipient_object_id'
|
|
)
|
|
|
|
# Badge
|
|
badge = models.ForeignKey(
|
|
AppreciationBadge,
|
|
on_delete=models.CASCADE,
|
|
related_name='earned_by'
|
|
)
|
|
|
|
# Achievement details
|
|
earned_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
# Context
|
|
appreciation_count = models.IntegerField(
|
|
help_text="Count when badge was earned"
|
|
)
|
|
|
|
# Metadata
|
|
metadata = models.JSONField(default=dict, blank=True)
|
|
|
|
class Meta:
|
|
ordering = ['-earned_at']
|
|
indexes = [
|
|
models.Index(fields=['recipient_content_type', 'recipient_object_id', '-earned_at']),
|
|
]
|
|
|
|
def __str__(self):
|
|
recipient_name = self.get_recipient_name()
|
|
return f"{recipient_name} - {self.badge.name_en}"
|
|
|
|
def get_recipient_name(self):
|
|
"""Get recipient's name"""
|
|
try:
|
|
return str(self.recipient)
|
|
except:
|
|
return "Unknown"
|
|
|
|
|
|
class AppreciationStats(UUIDModel, TimeStampedModel):
|
|
"""
|
|
Appreciation statistics - aggregated monthly.
|
|
|
|
Provides monthly statistics for users and physicians.
|
|
"""
|
|
# Recipient (Generic FK to User or Physician)
|
|
recipient_content_type = models.ForeignKey(
|
|
ContentType,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='appreciation_stats_recipients'
|
|
)
|
|
recipient_object_id = models.UUIDField(null=True, blank=True)
|
|
recipient = GenericForeignKey(
|
|
'recipient_content_type',
|
|
'recipient_object_id'
|
|
)
|
|
|
|
# Time period
|
|
year = models.IntegerField(db_index=True)
|
|
month = models.IntegerField(db_index=True, help_text="1-12")
|
|
|
|
# Organization
|
|
hospital = models.ForeignKey(
|
|
'organizations.Hospital',
|
|
on_delete=models.CASCADE,
|
|
related_name='appreciation_stats'
|
|
)
|
|
department = models.ForeignKey(
|
|
'organizations.Department',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='appreciation_stats'
|
|
)
|
|
|
|
# Statistics
|
|
received_count = models.IntegerField(default=0)
|
|
sent_count = models.IntegerField(default=0)
|
|
acknowledged_count = models.IntegerField(default=0)
|
|
|
|
# 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"
|
|
)
|
|
|
|
# Breakdown by category
|
|
category_breakdown = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text="Breakdown by category ID and count"
|
|
)
|
|
|
|
# Metadata
|
|
metadata = models.JSONField(default=dict, blank=True)
|
|
|
|
class Meta:
|
|
ordering = ['-year', '-month', '-received_count']
|
|
unique_together = [
|
|
['recipient_content_type', 'recipient_object_id', 'year', 'month']
|
|
]
|
|
indexes = [
|
|
models.Index(fields=['hospital', 'year', 'month', '-received_count']),
|
|
models.Index(fields=['department', 'year', 'month', '-received_count']),
|
|
]
|
|
|
|
def __str__(self):
|
|
recipient_name = self.get_recipient_name()
|
|
return f"{recipient_name} - {self.year}-{self.month:02d}: {self.received_count} received"
|
|
|
|
def get_recipient_name(self):
|
|
"""Get recipient's name"""
|
|
try:
|
|
return str(self.recipient)
|
|
except:
|
|
return "Unknown"
|