""" 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)