481 lines
17 KiB
Python
481 lines
17 KiB
Python
"""
|
|
Appreciation signals - Signal handlers for appreciation events
|
|
|
|
This module handles:
|
|
- Sending notifications when appreciations are sent
|
|
- Updating statistics when appreciations are created
|
|
- Checking and awarding badges
|
|
"""
|
|
from django.db.models import Q
|
|
from django.db.models.signals import post_save
|
|
from django.dispatch import receiver
|
|
from django.utils import timezone
|
|
|
|
from apps.appreciation.models import (
|
|
Appreciation,
|
|
AppreciationBadge,
|
|
AppreciationStats,
|
|
AppreciationStatus,
|
|
UserBadge,
|
|
)
|
|
|
|
|
|
@receiver(post_save, sender=Appreciation)
|
|
def handle_appreciation_sent(sender, instance, created, **kwargs):
|
|
"""
|
|
Handle appreciation sent events.
|
|
|
|
This signal triggers:
|
|
1. Send notification to recipient
|
|
2. Update statistics
|
|
3. Check for badge awards
|
|
"""
|
|
# Only process when appreciation is sent (status changes to 'sent')
|
|
if instance.status == AppreciationStatus.SENT and not instance.notification_sent:
|
|
# Send notification
|
|
send_appreciation_notification(instance)
|
|
|
|
# Update statistics
|
|
update_appreciation_stats(instance)
|
|
|
|
# Check for badge awards
|
|
check_and_award_badges(instance)
|
|
|
|
|
|
def send_appreciation_notification(appreciation):
|
|
"""
|
|
Send notification to recipient when appreciation is sent.
|
|
|
|
Uses the notification system to send email/SMS/WhatsApp.
|
|
Also sends email to department heads.
|
|
"""
|
|
try:
|
|
from apps.notifications.services import send_email, send_sms
|
|
|
|
recipient_email = appreciation.get_recipient_email()
|
|
recipient_phone = appreciation.get_recipient_phone()
|
|
|
|
sender_name = "Anonymous" if appreciation.is_anonymous else (
|
|
appreciation.sender.get_full_name() if appreciation.sender
|
|
else (appreciation.metadata or {}).get("submitted_by_name", "Anonymous")
|
|
)
|
|
|
|
html_message = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head><style>
|
|
body {{ font-family: 'Segoe UI', Tahoma, sans-serif; background: #f8fafc; margin: 0; padding: 20px; }}
|
|
.container {{ max-width: 600px; margin: 0 auto; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }}
|
|
.header {{ background: linear-gradient(135deg, #005696 0%, #007bbd 100%); padding: 30px; text-align: center; color: white; }}
|
|
.content {{ padding: 30px; }}
|
|
.appreciation-box {{ background: #eef6fb; border-left: 4px solid #005696; padding: 20px; margin: 20px 0; }}
|
|
.personal-note {{ background: #fffbeb; border-left: 4px solid #f59e0b; padding: 20px; margin: 20px 0; }}
|
|
</style></head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🌟 Staff Appreciation</h1>
|
|
</div>
|
|
<div class="content">
|
|
<div class="appreciation-box">
|
|
<strong>From:</strong> {sender_name}<br>
|
|
<strong>Hospital:</strong> {appreciation.hospital.name}<br>
|
|
{f'<strong>Category:</strong> {appreciation.category.name_en}<br>' if appreciation.category else ''}
|
|
<br>
|
|
<strong>Message:</strong><br>
|
|
{appreciation.message_en}
|
|
</div>
|
|
{f'<div class="personal-note"><strong>Personal Note from {sender_name}:</strong><br>{appreciation.custom_message}</div>' if appreciation.custom_message else ''}
|
|
<p>Congratulations on this recognition! Your dedication is truly appreciated.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
message_en = f"You've received an appreciation from {sender_name}!"
|
|
message_ar = f"لقد تلقيت تكريماً من {sender_name}!"
|
|
|
|
if appreciation.category:
|
|
message_en += f"\n\nCategory: {appreciation.category.name_en}"
|
|
message_ar += f"\n\nالفئة: {appreciation.category.name_ar}"
|
|
|
|
message_en += f"\n\nMessage: {appreciation.message_en}"
|
|
message_ar += f"\n\nالرسالة: {appreciation.message_ar}"
|
|
|
|
if appreciation.custom_message:
|
|
message_en += f"\n\nPersonal note: {appreciation.custom_message}"
|
|
message_ar += f"\n\nملاحظة شخصية: {appreciation.custom_message}"
|
|
|
|
if recipient_email:
|
|
try:
|
|
send_email(
|
|
email=recipient_email,
|
|
subject=f"New Appreciation Received - {appreciation.hospital.name}",
|
|
message=message_en,
|
|
html_message=html_message,
|
|
related_object=appreciation,
|
|
)
|
|
except Exception as e:
|
|
print(f"Failed to send appreciation email: {e}")
|
|
|
|
if recipient_phone:
|
|
try:
|
|
send_sms(
|
|
phone=recipient_phone,
|
|
message=message_en,
|
|
related_object=appreciation,
|
|
)
|
|
except Exception as e:
|
|
print(f"Failed to send appreciation SMS: {e}")
|
|
|
|
_send_department_head_notification(appreciation, sender_name)
|
|
_send_cc_notifications(appreciation, sender_name, html_message, message_en)
|
|
|
|
except ImportError as e:
|
|
print(f"Notification service not available: {e}")
|
|
except Exception as e:
|
|
print(f"Error sending appreciation notification: {e}")
|
|
|
|
appreciation.notification_sent = True
|
|
appreciation.notification_sent_at = timezone.now()
|
|
appreciation.save(update_fields=['notification_sent', 'notification_sent_at'])
|
|
|
|
|
|
def _send_department_head_notification(appreciation, sender_name):
|
|
from apps.notifications.services import send_email as notify_send_email
|
|
from apps.organizations.models import Staff
|
|
|
|
if not appreciation.department:
|
|
return
|
|
|
|
dept = appreciation.department
|
|
recipient_emails = set()
|
|
|
|
if dept.manager and dept.manager.email:
|
|
recipient_emails.add(dept.manager.email)
|
|
|
|
heads = Staff.objects.filter(
|
|
department=dept, is_head=True
|
|
).select_related("user")
|
|
for head in heads:
|
|
if head.user and head.user.email:
|
|
recipient_emails.add(head.user.email)
|
|
|
|
if not recipient_emails:
|
|
return
|
|
|
|
recipient_name = appreciation.get_recipient_name() if appreciation.recipient else "N/A"
|
|
|
|
html = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head><style>
|
|
body {{ font-family: 'Segoe UI', Tahoma, sans-serif; background: #f8fafc; margin: 0; padding: 20px; }}
|
|
.container {{ max-width: 600px; margin: 0 auto; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }}
|
|
.header {{ background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; text-align: center; color: white; }}
|
|
.content {{ padding: 30px; }}
|
|
.info-box {{ background: #ecfdf5; border-left: 4px solid #10b981; padding: 20px; margin: 20px 0; }}
|
|
</style></head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🌟 New Appreciation in Your Department</h1>
|
|
</div>
|
|
<div class="content">
|
|
<div class="info-box">
|
|
<strong>Staff Member:</strong> {recipient_name}<br>
|
|
<strong>Department:</strong> {dept.name_en or dept.name}<br>
|
|
<strong>From:</strong> {sender_name}<br>
|
|
{f'<strong>Category:</strong> {appreciation.category.name_en}<br>' if appreciation.category else ''}
|
|
<br>
|
|
<strong>Message:</strong><br>
|
|
{appreciation.message_en}
|
|
</div>
|
|
<p>A staff member in your department has received an appreciation. Please acknowledge their good work.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
plain_message = (
|
|
f"New appreciation for {recipient_name} in {dept.name_en or dept.name}.\n"
|
|
f"From: {sender_name}\n\n"
|
|
f"Message: {appreciation.message_en}"
|
|
)
|
|
|
|
for email in recipient_emails:
|
|
try:
|
|
notify_send_email(
|
|
email=email,
|
|
subject=f"New Appreciation for {recipient_name} - {dept.name_en or dept.name}",
|
|
message=plain_message,
|
|
html_message=html,
|
|
related_object=appreciation,
|
|
)
|
|
except Exception as e:
|
|
print(f"Failed to send department head notification: {e}")
|
|
|
|
|
|
def _send_cc_notifications(appreciation, sender_name, html_message, plain_message):
|
|
from apps.notifications.services import send_email as notify_send_email
|
|
|
|
cc_list = appreciation.cc_list or []
|
|
if not cc_list:
|
|
return
|
|
|
|
recipient_name = appreciation.get_recipient_name() if appreciation.recipient else "N/A"
|
|
|
|
for cc_email in cc_list:
|
|
if not cc_email:
|
|
continue
|
|
try:
|
|
notify_send_email(
|
|
email=cc_email.strip(),
|
|
subject=f"CC: Appreciation sent to {recipient_name} - {appreciation.hospital.name}",
|
|
message=plain_message,
|
|
html_message=html_message,
|
|
related_object=appreciation,
|
|
)
|
|
except Exception as e:
|
|
print(f"Failed to send CC notification to {cc_email}: {e}")
|
|
|
|
|
|
def update_appreciation_stats(instance):
|
|
"""
|
|
Update appreciation statistics for the recipient.
|
|
|
|
Creates or updates monthly statistics.
|
|
"""
|
|
# Get current year and month
|
|
now = timezone.now()
|
|
year = now.year
|
|
month = now.month
|
|
|
|
# Get or create stats record
|
|
stats, created = AppreciationStats.objects.get_or_create(
|
|
recipient_content_type=instance.recipient_content_type,
|
|
recipient_object_id=instance.recipient_object_id,
|
|
year=year,
|
|
month=month,
|
|
defaults={
|
|
'hospital': instance.hospital,
|
|
'department': instance.department,
|
|
'received_count': 0,
|
|
'sent_count': 0,
|
|
'acknowledged_count': 0,
|
|
'category_breakdown': {},
|
|
}
|
|
)
|
|
|
|
# Update received count
|
|
stats.received_count += 1
|
|
|
|
# Update category breakdown
|
|
if instance.category:
|
|
category_breakdown = stats.category_breakdown or {}
|
|
category_id_str = str(instance.category.id)
|
|
category_breakdown[category_id_str] = category_breakdown.get(category_id_str, 0) + 1
|
|
stats.category_breakdown = category_breakdown
|
|
|
|
# Save stats
|
|
stats.save()
|
|
|
|
# Recalculate rankings
|
|
recalculate_rankings(instance.hospital, year, month, instance.department)
|
|
|
|
|
|
def recalculate_rankings(hospital, year, month, department=None):
|
|
"""
|
|
Recalculate rankings for a given period.
|
|
|
|
Updates hospital_rank and department_rank for all recipients.
|
|
"""
|
|
# Get all stats for the period
|
|
if department:
|
|
stats_queryset = AppreciationStats.objects.filter(
|
|
hospital=hospital,
|
|
department=department,
|
|
year=year,
|
|
month=month,
|
|
)
|
|
else:
|
|
stats_queryset = AppreciationStats.objects.filter(
|
|
hospital=hospital,
|
|
year=year,
|
|
month=month,
|
|
)
|
|
|
|
# Order by received count
|
|
stats_queryset = stats_queryset.order_by('-received_count')
|
|
|
|
# Update hospital rank
|
|
for rank, stat in enumerate(stats_queryset, start=1):
|
|
stat.hospital_rank = rank
|
|
stat.save(update_fields=['hospital_rank'])
|
|
|
|
# Update department rank if department is specified
|
|
if department:
|
|
dept_stats = stats_queryset.filter(department=department)
|
|
for rank, stat in enumerate(dept_stats, start=1):
|
|
stat.department_rank = rank
|
|
stat.save(update_fields=['department_rank'])
|
|
|
|
|
|
def check_and_award_badges(instance):
|
|
"""
|
|
Check if recipient qualifies for any badges and award them.
|
|
|
|
Checks all active badge criteria and awards badges if criteria are met.
|
|
"""
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
# Get recipient
|
|
recipient_content_type = instance.recipient_content_type
|
|
recipient_object_id = instance.recipient_object_id
|
|
|
|
# Get all active badges for the hospital
|
|
badges = AppreciationBadge.objects.filter(
|
|
Q(hospital=instance.hospital) | Q(hospital__isnull=True),
|
|
is_active=True,
|
|
)
|
|
|
|
for badge in badges:
|
|
# Check if badge already earned
|
|
if UserBadge.objects.filter(
|
|
recipient_content_type=recipient_content_type,
|
|
recipient_object_id=recipient_object_id,
|
|
badge=badge,
|
|
).exists():
|
|
continue # Already earned
|
|
|
|
# Check badge criteria
|
|
qualifies = check_badge_criteria(
|
|
badge,
|
|
recipient_content_type,
|
|
recipient_object_id,
|
|
instance.hospital,
|
|
)
|
|
|
|
if qualifies:
|
|
# Calculate current count
|
|
count = get_appreciation_count(
|
|
recipient_content_type,
|
|
recipient_object_id,
|
|
badge.criteria_type
|
|
)
|
|
|
|
# Award badge
|
|
UserBadge.objects.create(
|
|
recipient_content_type=recipient_content_type,
|
|
recipient_object_id=recipient_object_id,
|
|
badge=badge,
|
|
appreciation_count=count,
|
|
)
|
|
|
|
|
|
def check_badge_criteria(badge, content_type, object_id, hospital):
|
|
"""
|
|
Check if recipient meets badge criteria.
|
|
|
|
Returns True if criteria is met, False otherwise.
|
|
"""
|
|
criteria_type = badge.criteria_type
|
|
criteria_value = badge.criteria_value
|
|
|
|
if criteria_type == 'received_count':
|
|
# Check total appreciation count
|
|
count = Appreciation.objects.filter(
|
|
recipient_content_type=content_type,
|
|
recipient_object_id=object_id,
|
|
status=AppreciationStatus.SENT,
|
|
).count()
|
|
return count >= criteria_value
|
|
|
|
elif criteria_type == 'received_month':
|
|
# Check appreciation count in current month
|
|
now = timezone.now()
|
|
count = Appreciation.objects.filter(
|
|
recipient_content_type=content_type,
|
|
recipient_object_id=object_id,
|
|
status=AppreciationStatus.SENT,
|
|
sent_at__year=now.year,
|
|
sent_at__month=now.month,
|
|
).count()
|
|
return count >= criteria_value
|
|
|
|
elif criteria_type == 'streak_weeks':
|
|
# Check consecutive weeks with appreciation
|
|
return check_appreciation_streak(
|
|
content_type,
|
|
object_id,
|
|
criteria_value
|
|
)
|
|
|
|
elif criteria_type == 'diverse_senders':
|
|
# Check appreciation from different senders
|
|
sender_count = Appreciation.objects.filter(
|
|
recipient_content_type=content_type,
|
|
recipient_object_id=object_id,
|
|
status=AppreciationStatus.SENT,
|
|
).values('sender').distinct().count()
|
|
return sender_count >= criteria_value
|
|
|
|
return False
|
|
|
|
|
|
def get_appreciation_count(content_type, object_id, criteria_type):
|
|
"""
|
|
Get appreciation count based on criteria type.
|
|
"""
|
|
if criteria_type == 'received_count':
|
|
return Appreciation.objects.filter(
|
|
recipient_content_type=content_type,
|
|
recipient_object_id=object_id,
|
|
status=AppreciationStatus.SENT,
|
|
).count()
|
|
elif criteria_type == 'received_month':
|
|
now = timezone.now()
|
|
return Appreciation.objects.filter(
|
|
recipient_content_type=content_type,
|
|
recipient_object_id=object_id,
|
|
status=AppreciationStatus.SENT,
|
|
sent_at__year=now.year,
|
|
sent_at__month=now.month,
|
|
).count()
|
|
return 0
|
|
|
|
|
|
def check_appreciation_streak(content_type, object_id, required_weeks):
|
|
"""
|
|
Check if recipient has appreciation streak for required weeks.
|
|
|
|
Returns True if streak meets or exceeds required_weeks.
|
|
"""
|
|
from datetime import timedelta
|
|
|
|
now = timezone.now()
|
|
current_week = 0
|
|
|
|
# Check week by week going backwards
|
|
for i in range(required_weeks):
|
|
week_start = now - timedelta(weeks=i+1)
|
|
week_end = now - timedelta(weeks=i)
|
|
|
|
# Check if there's any appreciation in this week
|
|
has_appreciation = Appreciation.objects.filter(
|
|
recipient_content_type=content_type,
|
|
recipient_object_id=object_id,
|
|
status=AppreciationStatus.SENT,
|
|
sent_at__gte=week_start,
|
|
sent_at__lt=week_end,
|
|
).exists()
|
|
|
|
if has_appreciation:
|
|
current_week += 1
|
|
else:
|
|
break
|
|
|
|
return current_week >= required_weeks
|