HH/apps/notifications/views.py
2026-03-28 14:03:56 +03:00

701 lines
25 KiB
Python

"""
Notification Settings Views
Provides admin interface for configuring notification preferences.
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator
from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST
from apps.organizations.models import Hospital
from .settings_models import HospitalNotificationSettings, NotificationSettingsLog
def can_manage_notifications(user):
"""Check if user can manage notification settings"""
if user.is_superuser:
return True
if hasattr(user, "is_px_admin") and user.is_px_admin():
return True
if hasattr(user, "is_hospital_admin") and user.is_hospital_admin():
return True
return False
@login_required
def notification_settings_view(request, hospital_id=None):
"""
Notification settings configuration page.
Allows hospital admins to toggle notification preferences
for different events and channels.
"""
# Check permission
if not can_manage_notifications(request.user):
raise PermissionDenied("You do not have permission to manage notification settings.")
# Get hospital - if superuser, can view any; otherwise only their hospital
if request.user.is_superuser and hospital_id:
hospital = get_object_or_404(Hospital, id=hospital_id)
else:
hospital = request.user.hospital
hospital_id = hospital.id
# Get or create settings
settings = HospitalNotificationSettings.get_for_hospital(hospital_id)
# Group settings by category for display
notification_categories = [
{
"key": "complaint",
"name": "Complaint Notifications",
"icon": "bi-exclamation-triangle",
"description": "Notifications related to complaint lifecycle",
"events": [
{
"key": "complaint_acknowledgment",
"name": "Complaint Acknowledgment",
"description": "Sent to patient when complaint is acknowledged",
"icon": "bi-check-circle",
"channels": ["email", "sms", "whatsapp"],
},
{
"key": "complaint_assigned",
"name": "Complaint Assigned",
"description": "Sent to staff when complaint is assigned to them",
"icon": "bi-person-check",
"channels": ["email", "sms", "whatsapp"],
},
{
"key": "complaint_status_changed",
"name": "Complaint Status Changed",
"description": "Sent when complaint status is updated",
"icon": "bi-arrow-repeat",
"channels": ["email", "sms", "whatsapp"],
},
{
"key": "complaint_resolved",
"name": "Complaint Resolved",
"description": "Sent to patient when complaint is resolved",
"icon": "bi-check2-all",
"channels": ["email", "sms", "whatsapp"],
},
{
"key": "complaint_closed",
"name": "Complaint Closed",
"description": "Sent to patient when complaint is closed",
"icon": "bi-x-circle",
"channels": ["email", "sms", "whatsapp"],
},
],
},
{
"key": "explanation",
"name": "Explanation Notifications",
"icon": "bi-chat-left-text",
"description": "Notifications for staff explanation workflow",
"events": [
{
"key": "explanation_requested",
"name": "Explanation Requested",
"description": "Sent to staff when explanation is requested",
"icon": "bi-question-circle",
"channels": ["email", "sms", "whatsapp"],
},
{
"key": "explanation_reminder",
"name": "Explanation Reminder (24h)",
"description": "Reminder sent 24h before SLA deadline",
"icon": "bi-clock-history",
"channels": ["email", "sms", "whatsapp"],
},
{
"key": "explanation_overdue",
"name": "Explanation Overdue/Escalation",
"description": "Sent to manager when explanation is overdue",
"icon": "bi-exclamation-diamond",
"channels": ["email", "sms", "whatsapp"],
},
{
"key": "explanation_received",
"name": "Explanation Received",
"description": "Sent to assignee when staff submits explanation",
"icon": "bi-inbox",
"channels": ["email", "sms", "whatsapp"],
},
],
"extra_settings": [
{
"key": "explanation_manager_cc",
"name": "Manager CC on Explanation Request",
"description": "CC manager when explanation is requested from staff",
"icon": "bi-person-badge",
}
],
},
{
"key": "survey",
"name": "Survey Notifications",
"icon": "bi-clipboard-check",
"description": "Notifications for patient surveys",
"events": [
{
"key": "survey_invitation",
"name": "Survey Invitation",
"description": "Initial survey invitation sent to patient",
"icon": "bi-envelope-open",
"channels": ["email", "sms", "whatsapp"],
},
{
"key": "survey_reminder",
"name": "Survey Reminder",
"description": "Reminder for patients to complete survey",
"icon": "bi-bell",
"channels": ["email", "sms", "whatsapp"],
},
{
"key": "survey_completed",
"name": "Survey Completed",
"description": "Sent to admin when survey is completed",
"icon": "bi-check-circle-fill",
"channels": ["email", "sms"],
},
],
},
{
"key": "action",
"name": "Action Plan Notifications",
"icon": "bi-list-check",
"description": "Notifications for action plan assignments",
"events": [
{
"key": "action_assigned",
"name": "Action Assigned",
"description": "Sent to staff when action is assigned",
"icon": "bi-person-plus",
"channels": ["email", "sms", "whatsapp"],
},
{
"key": "action_due_soon",
"name": "Action Due Soon",
"description": "Reminder when action is approaching deadline",
"icon": "bi-calendar-event",
"channels": ["email", "sms"],
},
{
"key": "action_overdue",
"name": "Action Overdue",
"description": "Alert when action is past deadline",
"icon": "bi-calendar-x",
"channels": ["email", "sms"],
},
],
},
{
"key": "sla",
"name": "SLA Notifications",
"icon": "bi-stopwatch",
"description": "Notifications for SLA monitoring",
"events": [
{
"key": "sla_reminder",
"name": "SLA Reminder",
"description": "Reminder before SLA breach",
"icon": "bi-clock",
"channels": ["email", "sms"],
},
{
"key": "sla_breach",
"name": "SLA Breach Alert",
"description": "Alert when SLA is breached",
"icon": "bi-exclamation-octagon",
"channels": ["email", "sms", "whatsapp"],
},
],
},
{
"key": "onboarding",
"name": "Onboarding Notifications",
"icon": "bi-person-plus",
"description": "Notifications for user onboarding and acknowledgements",
"events": [
{
"key": "onboarding_invitation",
"name": "Onboarding Invitation",
"description": "Sent to new provisional users to complete registration",
"icon": "bi-envelope-plus",
"channels": ["email", "sms", "whatsapp"],
},
{
"key": "onboarding_reminder",
"name": "Onboarding Reminder",
"description": "Reminder to complete onboarding before invitation expires",
"icon": "bi-bell",
"channels": ["email", "sms", "whatsapp"],
},
{
"key": "onboarding_completion",
"name": "Onboarding Completion",
"description": "Sent to admins when user completes onboarding",
"icon": "bi-check-circle-fill",
"channels": ["email", "sms"],
},
],
},
]
# Get recent change logs
change_logs = (
NotificationSettingsLog.objects.filter(hospital=hospital)
.select_related("changed_by")
.order_by("-created_at")[:10]
)
context = {
"hospital": hospital,
"settings": settings,
"categories": notification_categories,
"change_logs": change_logs,
"is_superuser": request.user.is_superuser,
}
return render(request, "notifications/settings.html", context)
@login_required
@require_POST
def notification_settings_update(request, hospital_id=None):
"""
Update notification settings via AJAX or form POST.
"""
# Check permission
if not can_manage_notifications(request.user):
raise PermissionDenied("You do not have permission to manage notification settings.")
# Get hospital
if request.user.is_superuser and hospital_id:
hospital = get_object_or_404(Hospital, id=hospital_id)
else:
hospital = request.user.hospital
settings = HospitalNotificationSettings.get_for_hospital(hospital.id)
# Handle master switch
if "notifications_enabled" in request.POST:
old_value = settings.notifications_enabled
new_value = request.POST.get("notifications_enabled") == "on"
settings.notifications_enabled = new_value
settings.save()
NotificationSettingsLog.objects.create(
hospital=hospital,
changed_by=request.user,
field_name="notifications_enabled",
old_value=old_value,
new_value=new_value,
)
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({"success": True, "message": f"Notifications {'enabled' if new_value else 'disabled'}"})
messages.success(request, f"Notifications {'enabled' if new_value else 'disabled'}")
return redirect("notifications:settings_with_hospital", hospital_id=hospital.id)
# Handle individual toggle
field_name = request.POST.get("field")
value = request.POST.get("value") == "true"
if field_name and hasattr(settings, field_name):
old_value = getattr(settings, field_name)
setattr(settings, field_name, value)
settings.save()
# Log the change
NotificationSettingsLog.objects.create(
hospital=hospital, changed_by=request.user, field_name=field_name, old_value=old_value, new_value=value
)
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({"success": True, "field": field_name, "value": value})
messages.success(request, "Setting updated successfully")
else:
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({"success": False, "error": "Invalid field name"}, status=400)
messages.error(request, "Invalid setting")
return redirect("notifications:settings_with_hospital", hospital_id=hospital.id)
@login_required
@require_POST
def update_quiet_hours(request, hospital_id=None):
"""
Update quiet hours settings.
"""
# Check permission
if not can_manage_notifications(request.user):
raise PermissionDenied("You do not have permission to manage notification settings.")
if request.user.is_superuser and hospital_id:
hospital = get_object_or_404(Hospital, id=hospital_id)
else:
hospital = request.user.hospital
settings = HospitalNotificationSettings.get_for_hospital(hospital.id)
settings.quiet_hours_enabled = request.POST.get("quiet_hours_enabled") == "on"
settings.quiet_hours_start = request.POST.get("quiet_hours_start", "22:00")
settings.quiet_hours_end = request.POST.get("quiet_hours_end", "08:00")
settings.save()
messages.success(request, "Quiet hours settings updated")
return redirect("notifications:settings_with_hospital", hospital_id=hospital.id)
@login_required
def test_notification(request, hospital_id=None):
"""
Send a test notification to verify settings.
"""
# Check permission
if not can_manage_notifications(request.user):
raise PermissionDenied("You do not have permission to manage notification settings.")
if request.user.is_superuser and hospital_id:
hospital = get_object_or_404(Hospital, id=hospital_id)
else:
hospital = request.user.hospital
settings = HospitalNotificationSettings.get_for_hospital(hospital.id)
channel = request.POST.get("channel", "email")
from .services import NotificationService
if channel == "email" and request.user.email:
NotificationService.send_email(
email=request.user.email,
subject="PX360 Test Notification",
message="This is a test notification from PX360.\n\nIf you received this, your email notifications are working correctly.",
metadata={"test": True, "hospital_id": str(hospital.id)},
)
messages.success(request, f"Test email sent to {request.user.email}")
elif channel == "sms":
phone = request.POST.get("test_phone")
if phone:
NotificationService.send_sms(
phone=phone,
message="PX360 Test: Your SMS notifications are configured correctly.",
metadata={"test": True, "hospital_id": str(hospital.id)},
)
messages.success(request, f"Test SMS sent to {phone}")
else:
messages.error(request, "Please provide a phone number")
else:
messages.error(request, "Invalid channel selected")
return redirect("notifications:settings_with_hospital", hospital_id=hospital.id)
@login_required
def notification_settings_api(request, hospital_id=None):
"""
API endpoint to get current notification settings as JSON.
Useful for AJAX updates and mobile apps.
"""
if request.user.is_superuser and hospital_id:
hospital = get_object_or_404(Hospital, id=hospital_id)
else:
hospital = request.user.hospital
settings = HospitalNotificationSettings.get_for_hospital(hospital.id)
# Build response with all settings
settings_dict = {}
for field in settings._meta.fields:
if field.name not in ["id", "uuid", "created_at", "updated_at", "hospital"]:
settings_dict[field.name] = getattr(settings, field.name)
return JsonResponse({"hospital_id": str(hospital.id), "hospital_name": hospital.name, "settings": settings_dict})
@login_required
def send_sms_direct(request):
"""
Direct SMS sending page for admins.
Allows PX Admins and Hospital Admins to send SMS messages
directly to any phone number.
"""
from .services import NotificationService
# Check permission - only admins can send direct SMS
if not can_manage_notifications(request.user):
raise PermissionDenied("You do not have permission to send SMS messages.")
if request.method == "POST":
phone_number = request.POST.get("phone_number", "").strip()
message = request.POST.get("message", "").strip()
# Validate inputs
errors = []
if not phone_number:
errors.append(_("Phone number is required."))
elif not phone_number.startswith("+"):
errors.append(_("Phone number must include country code (e.g., +966501234567)."))
if not message:
errors.append(_("Message is required."))
elif len(message) > 1600:
errors.append(_("Message is too long. Maximum 1600 characters."))
if errors:
for error in errors:
messages.error(request, error)
return render(
request,
"notifications/send_sms_direct.html",
{
"phone_number": phone_number,
"message": message,
},
)
try:
# Clean phone number
phone_number = phone_number.replace(" ", "").replace("-", "").replace("(", "").replace(")", "")
# Send SMS
notification_log = NotificationService.send_sms(
phone=phone_number,
message=message,
metadata={
"sent_by": str(request.user.id),
"sent_by_name": request.user.get_full_name(),
"source": "direct_sms_send",
},
)
# Log the action
from apps.core.services import AuditService
AuditService.log_event(
event_type="sms_sent_direct",
description=f"Direct SMS sent to {phone_number} by {request.user.get_full_name()}",
user=request.user,
metadata={
"phone_number": phone_number,
"message_length": len(message),
"notification_log_id": str(notification_log.id) if notification_log else None,
},
)
messages.success(request, _(f"SMS sent successfully to {phone_number}."))
return redirect("notifications:send_sms_direct")
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Error sending direct SMS: {str(e)}", exc_info=True)
messages.error(request, f"Error sending SMS: {str(e)}")
return render(request, "notifications/send_sms_direct.html")
# ============================================================================
# NOTIFICATION INBOX VIEWS
# ============================================================================
@login_required
def notification_inbox(request):
"""
Notification inbox page.
Displays all non-dismissed notifications for the current user.
"""
from .models import UserNotification
filter_type = request.GET.get("filter", "all")
# Base queryset - exclude dismissed
notifications = UserNotification.objects.filter(user=request.user, is_dismissed=False)
# Apply filters
if filter_type == "unread":
notifications = notifications.filter(is_read=False)
elif filter_type == "read":
notifications = notifications.filter(is_read=True)
# Pagination
paginator = Paginator(notifications, 20)
page = request.GET.get("page", 1)
notifications_page = paginator.get_page(page)
context = {
"notifications": notifications_page,
"filter": filter_type,
"unread_count": UserNotification.objects.filter(user=request.user, is_read=False, is_dismissed=False).count(),
}
return render(request, "notifications/inbox.html", context)
@login_required
def notification_list_api(request):
"""
API endpoint to get notifications list (JSON).
"""
from .models import UserNotification
from django.http import JsonResponse
filter_type = request.GET.get("filter", "all")
page = int(request.GET.get("page", 1))
notifications = UserNotification.objects.filter(user=request.user, is_dismissed=False)
if filter_type == "unread":
notifications = notifications.filter(is_read=False)
elif filter_type == "read":
notifications = notifications.filter(is_read=True)
paginator = Paginator(notifications, 20)
notifications_page = paginator.get_page(page)
data = {
"notifications": [
{
"id": str(n.id),
"title": n.get_title(),
"message": n.get_message(),
"type": n.notification_type,
"is_read": n.is_read,
"created_at": n.created_at.isoformat(),
"action_url": n.action_url,
}
for n in notifications_page
],
"has_next": notifications_page.has_next(),
"has_previous": notifications_page.has_previous(),
"page": page,
"total_pages": paginator.num_pages,
}
return JsonResponse(data)
@login_required
@require_POST
def mark_notification_read(request, notification_id):
"""
Mark a single notification as read.
"""
from .models import UserNotification
from django.http import JsonResponse
try:
notification = UserNotification.objects.get(id=notification_id, user=request.user)
notification.mark_as_read()
return JsonResponse({"success": True})
except UserNotification.DoesNotExist:
return JsonResponse({"success": False, "error": "Notification not found"}, status=404)
@login_required
@require_POST
def mark_all_notifications_read(request):
"""
Mark all notifications as read.
"""
from .models import UserNotification
from django.http import JsonResponse
from django.utils import timezone
UserNotification.objects.filter(user=request.user, is_read=False, is_dismissed=False).update(
is_read=True, read_at=timezone.now()
)
return JsonResponse({"success": True})
@login_required
@require_POST
def dismiss_notification(request, notification_id):
"""
Dismiss a notification.
"""
from .models import UserNotification
from django.http import JsonResponse
try:
notification = UserNotification.objects.get(id=notification_id, user=request.user)
notification.mark_as_dismissed()
return JsonResponse({"success": True})
except UserNotification.DoesNotExist:
return JsonResponse({"success": False, "error": "Notification not found"}, status=404)
@login_required
@require_POST
def dismiss_all_notifications(request):
"""
Dismiss all notifications.
"""
from .models import UserNotification
from django.http import JsonResponse
from django.utils import timezone
UserNotification.objects.filter(user=request.user, is_dismissed=False).update(
is_dismissed=True, dismissed_at=timezone.now()
)
return JsonResponse({"success": True})
@login_required
def unread_notification_count(request):
"""
Get unread notification count.
"""
from .models import UserNotification
from django.http import JsonResponse
count = UserNotification.objects.filter(user=request.user, is_read=False, is_dismissed=False).count()
return JsonResponse({"count": count})
@login_required
def latest_notifications(request):
"""
Get latest 5 unread notifications for dropdown.
"""
from .models import UserNotification
from django.http import JsonResponse
notifications = UserNotification.objects.filter(user=request.user, is_read=False, is_dismissed=False)[:5]
data = {
"notifications": [
{
"id": str(n.id),
"title": n.get_title(),
"message": n.get_message()[:100] + "..." if len(n.get_message()) > 100 else n.get_message(),
"type": n.notification_type,
"created_at": n.created_at.isoformat(),
"action_url": n.action_url,
}
for n in notifications
],
"has_more": UserNotification.objects.filter(user=request.user, is_read=False, is_dismissed=False).count() > 5,
}
return JsonResponse(data)