""" Notifications views for the Tenhal Multidisciplinary Healthcare Platform. This module contains views for notification management including: - Message dashboard with statistics - Message list and detail views - Template management (CRUD) - Bulk messaging interface - Analytics and reports """ from django.contrib.auth.mixins import LoginRequiredMixin from django.db.models import Q, Count, Avg from django.http import JsonResponse, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone from django.views import View from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView from django.urls import reverse_lazy from django.contrib import messages from datetime import timedelta import json import csv from core.mixins import ( TenantFilterMixin, RolePermissionMixin, AuditLogMixin, HTMXResponseMixin, SuccessMessageMixin, PaginationMixin, ) from core.models import User, Patient from .models import MessageTemplate, Message, NotificationPreference, MessageLog from .forms import ( MessageTemplateForm, MessageFilterForm, BulkMessageForm, TestTemplateForm, MessageRetryForm, ) # ============================================================================ # Dashboard Views # ============================================================================ class MessageDashboardView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View): """ Message dashboard with statistics and overview. Features: - Key metrics (total sent, success rate, etc.) - Recent messages - Charts for delivery rates - Quick actions """ allowed_roles = [User.Role.ADMIN, User.Role.FRONT_DESK] def get(self, request): """Display dashboard.""" tenant = request.user.tenant # Get date range (default: last 7 days) days = int(request.GET.get('days', 7)) since = timezone.now() - timedelta(days=days) # Get statistics stats = self._get_statistics(tenant, since) # Get recent messages recent_messages = Message.objects.filter( tenant=tenant ).select_related('template').order_by('-created_at')[:10] # Get failed messages that can be retried failed_messages = Message.objects.filter( tenant=tenant, status=Message.Status.FAILED, retry_count__lt=3 ).count() context = { 'stats': stats, 'recent_messages': recent_messages, 'failed_messages_count': failed_messages, 'days': days, } return render(request, 'notifications/dashboard.html', context) def _get_statistics(self, tenant, since): """Calculate messaging statistics.""" messages_qs = Message.objects.filter(tenant=tenant, created_at__gte=since) total = messages_qs.count() # Count by status sent = messages_qs.filter(status=Message.Status.SENT).count() delivered = messages_qs.filter(status=Message.Status.DELIVERED).count() failed = messages_qs.filter(status=Message.Status.FAILED).count() queued = messages_qs.filter(status=Message.Status.QUEUED).count() # Calculate success rate success_rate = 0 if total > 0: successful = messages_qs.filter( status__in=[Message.Status.DELIVERED, Message.Status.READ] ).count() success_rate = round((successful / total) * 100, 1) # Count by channel by_channel = {} for channel in Message.Channel: count = messages_qs.filter(channel=channel.value).count() by_channel[channel.value] = count # Get daily breakdown for chart daily_stats = [] for i in range(7): date = timezone.now().date() - timedelta(days=6-i) day_messages = messages_qs.filter(created_at__date=date) daily_stats.append({ 'date': date.strftime('%Y-%m-%d'), 'total': day_messages.count(), 'delivered': day_messages.filter(status=Message.Status.DELIVERED).count(), 'failed': day_messages.filter(status=Message.Status.FAILED).count(), }) return { 'total': total, 'sent': sent, 'delivered': delivered, 'failed': failed, 'queued': queued, 'success_rate': success_rate, 'by_channel': by_channel, 'daily_stats': daily_stats, } # ============================================================================ # Message List and Detail Views # ============================================================================ class MessageListView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, PaginationMixin, HTMXResponseMixin, ListView): """ Message list view with filtering and search. Features: - Filter by channel, status, date range, template - Search by recipient or content - Export to CSV - Bulk retry failed messages """ model = Message template_name = 'notifications/message_list.html' htmx_template_name = 'notifications/partials/message_list_partial.html' context_object_name = 'messages' paginate_by = 25 allowed_roles = [User.Role.ADMIN, User.Role.FRONT_DESK] def get_queryset(self): """Get filtered queryset.""" queryset = super().get_queryset() # Apply search search_query = self.request.GET.get('search', '').strip() if search_query: queryset = queryset.filter( Q(recipient__icontains=search_query) | Q(body__icontains=search_query) | Q(subject__icontains=search_query) ) # Apply filters channel = self.request.GET.get('channel') if channel: queryset = queryset.filter(channel=channel) status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) template_id = self.request.GET.get('template') if template_id: queryset = queryset.filter(template_id=template_id) date_from = self.request.GET.get('date_from') if date_from: queryset = queryset.filter(created_at__date__gte=date_from) date_to = self.request.GET.get('date_to') if date_to: queryset = queryset.filter(created_at__date__lte=date_to) return queryset.select_related('template').order_by('-created_at') def get_context_data(self, **kwargs): """Add filter form and statistics.""" context = super().get_context_data(**kwargs) # Add filter form context['filter_form'] = MessageFilterForm( self.request.GET, tenant=self.request.user.tenant ) # Add current filters context['current_filters'] = { 'search': self.request.GET.get('search', ''), 'channel': self.request.GET.get('channel', ''), 'status': self.request.GET.get('status', ''), 'template': self.request.GET.get('template', ''), 'date_from': self.request.GET.get('date_from', ''), 'date_to': self.request.GET.get('date_to', ''), } # Add quick stats queryset = self.get_queryset() context['total_count'] = queryset.count() context['failed_count'] = queryset.filter(status=Message.Status.FAILED).count() return context class MessageDetailView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, DetailView): """ Message detail view. Features: - Full message details - Delivery timeline - Provider response - Retry history - Related logs """ model = Message template_name = 'notifications/message_detail.html' context_object_name = 'message' allowed_roles = [User.Role.ADMIN, User.Role.FRONT_DESK] def get_context_data(self, **kwargs): """Add related data.""" context = super().get_context_data(**kwargs) message = self.object # Get message logs context['logs'] = message.logs.all().order_by('created_at') # Check if can retry context['can_retry'] = message.can_retry # Get timeline events context['timeline'] = self._build_timeline(message) return context def _build_timeline(self, message): """Build timeline of message events.""" timeline = [] # Created timeline.append({ 'timestamp': message.created_at, 'event': 'Created', 'icon': 'fa-plus-circle', 'color': 'info' }) # Sent if message.sent_at: timeline.append({ 'timestamp': message.sent_at, 'event': 'Sent', 'icon': 'fa-paper-plane', 'color': 'primary' }) # Delivered if message.delivered_at: timeline.append({ 'timestamp': message.delivered_at, 'event': 'Delivered', 'icon': 'fa-check-circle', 'color': 'success' }) # Failed if message.status == Message.Status.FAILED: timeline.append({ 'timestamp': message.updated_at, 'event': 'Failed', 'icon': 'fa-exclamation-circle', 'color': 'danger', 'details': message.error_message }) return sorted(timeline, key=lambda x: x['timestamp']) class MessageExportView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View): """Export messages to CSV.""" allowed_roles = [User.Role.ADMIN, User.Role.FRONT_DESK] def get(self, request): """Export filtered messages to CSV.""" # Get filtered queryset (reuse MessageListView logic) queryset = Message.objects.filter(tenant=request.user.tenant) # Apply same filters as list view search_query = request.GET.get('search', '').strip() if search_query: queryset = queryset.filter( Q(recipient__icontains=search_query) | Q(body__icontains=search_query) ) channel = request.GET.get('channel') if channel: queryset = queryset.filter(channel=channel) status = request.GET.get('status') if status: queryset = queryset.filter(status=status) # Create CSV response response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = f'attachment; filename="messages_{timezone.now().strftime("%Y%m%d_%H%M%S")}.csv"' writer = csv.writer(response) writer.writerow([ 'Date', 'Channel', 'Recipient', 'Status', 'Template', 'Sent At', 'Delivered At', 'Error Message' ]) for message in queryset.select_related('template').order_by('-created_at'): writer.writerow([ message.created_at.strftime('%Y-%m-%d %H:%M:%S'), message.get_channel_display(), message.recipient, message.get_status_display(), message.template.name if message.template else '', message.sent_at.strftime('%Y-%m-%d %H:%M:%S') if message.sent_at else '', message.delivered_at.strftime('%Y-%m-%d %H:%M:%S') if message.delivered_at else '', message.error_message ]) return response class MessageRetryView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, AuditLogMixin, View): """Retry failed message.""" allowed_roles = [User.Role.ADMIN, User.Role.FRONT_DESK] def post(self, request, pk): """Retry sending a failed message.""" from integrations.messaging_service import MessagingService message = get_object_or_404(Message, pk=pk, tenant=request.user.tenant) if not message.can_retry: messages.error(request, 'This message cannot be retried.') return redirect('notifications:message_detail', pk=pk) # Retry message service = MessagingService() result = service.retry_failed_message(str(message.id)) if result['success']: messages.success(request, 'Message retry initiated successfully!') else: messages.error(request, f'Retry failed: {result.get("error")}') return redirect('notifications:message_detail', pk=pk) # ============================================================================ # Template Management Views # ============================================================================ class TemplateListView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, PaginationMixin, ListView): """ Template list view. Features: - List all templates - Filter by channel, active status - Quick activate/deactivate """ model = MessageTemplate template_name = 'notifications/template_list.html' context_object_name = 'templates' paginate_by = 20 allowed_roles = [User.Role.ADMIN, User.Role.FRONT_DESK] def get_queryset(self): """Get filtered queryset.""" queryset = super().get_queryset() # Apply filters channel = self.request.GET.get('channel') if channel: queryset = queryset.filter(channel=channel) is_active = self.request.GET.get('is_active') if is_active: queryset = queryset.filter(is_active=is_active == 'true') return queryset.order_by('channel', 'name') def get_context_data(self, **kwargs): """Add filter options.""" context = super().get_context_data(**kwargs) context['channel_choices'] = MessageTemplate.Channel.choices return context class TemplateDetailView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, DetailView): """Template detail view with preview.""" model = MessageTemplate template_name = 'notifications/template_detail.html' context_object_name = 'template' allowed_roles = [User.Role.ADMIN, User.Role.FRONT_DESK] def get_context_data(self, **kwargs): """Add usage statistics.""" context = super().get_context_data(**kwargs) template = self.object # Get usage statistics context['usage_stats'] = { 'total_sent': template.messages.count(), 'successful': template.messages.filter( status__in=[Message.Status.DELIVERED, Message.Status.READ] ).count(), 'failed': template.messages.filter(status=Message.Status.FAILED).count(), } # Get recent messages using this template context['recent_messages'] = template.messages.all().order_by('-created_at')[:5] return context class TemplateCreateView(LoginRequiredMixin, RolePermissionMixin, AuditLogMixin, SuccessMessageMixin, CreateView): """Create new message template.""" model = MessageTemplate form_class = MessageTemplateForm template_name = 'notifications/template_form.html' success_message = "Template created successfully!" allowed_roles = [User.Role.ADMIN] def form_valid(self, form): """Set tenant.""" form.instance.tenant = self.request.user.tenant return super().form_valid(form) def get_success_url(self): """Redirect to template detail.""" return reverse_lazy('notifications:template_detail', kwargs={'pk': self.object.pk}) def get_context_data(self, **kwargs): """Add form title.""" context = super().get_context_data(**kwargs) context['form_title'] = 'Create Message Template' context['submit_text'] = 'Create Template' return context class TemplateUpdateView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, AuditLogMixin, SuccessMessageMixin, UpdateView): """Update message template.""" model = MessageTemplate form_class = MessageTemplateForm template_name = 'notifications/template_form.html' success_message = "Template updated successfully!" allowed_roles = [User.Role.ADMIN] def get_success_url(self): """Redirect to template detail.""" return reverse_lazy('notifications:template_detail', kwargs={'pk': self.object.pk}) def get_context_data(self, **kwargs): """Add form title.""" context = super().get_context_data(**kwargs) context['form_title'] = f'Update Template: {self.object.name}' context['submit_text'] = 'Update Template' return context class TemplateDeleteView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, AuditLogMixin, DeleteView): """Delete message template.""" model = MessageTemplate template_name = 'notifications/template_confirm_delete.html' success_url = reverse_lazy('notifications:template_list') allowed_roles = [User.Role.ADMIN] def delete(self, request, *args, **kwargs): """Add success message.""" messages.success(request, 'Template deleted successfully!') return super().delete(request, *args, **kwargs) class TemplateToggleView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, AuditLogMixin, View): """Toggle template active status.""" allowed_roles = [User.Role.ADMIN] def post(self, request, pk): """Toggle is_active status.""" template = get_object_or_404(MessageTemplate, pk=pk, tenant=request.user.tenant) template.is_active = not template.is_active template.save() status = 'activated' if template.is_active else 'deactivated' messages.success(request, f'Template {status} successfully!') return redirect('notifications:template_detail', pk=pk) class TemplateTestView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View): """Test message template.""" allowed_roles = [User.Role.ADMIN, User.Role.FRONT_DESK] def get(self, request): """Display test form.""" form = TestTemplateForm(tenant=request.user.tenant) return render(request, 'notifications/template_test.html', {'form': form}) def post(self, request): """Send test message.""" from integrations.messaging_service import MessagingService form = TestTemplateForm(request.POST, tenant=request.user.tenant) if form.is_valid(): template = form.cleaned_data['template'] recipient = form.cleaned_data['test_recipient'] language = form.cleaned_data['language'] variables = form.cleaned_data['variables'] # Send test message service = MessagingService() result = service.send_from_template( template_code=template.code, recipient_phone=recipient, channel=template.channel, context=variables, tenant_id=str(request.user.tenant.id), language=language ) if result['success']: messages.success(request, f'Test message sent successfully! Message ID: {result["message_id"]}') else: messages.error(request, f'Failed to send test message: {result.get("error")}') return render(request, 'notifications/template_test.html', {'form': form}) # ============================================================================ # Bulk Messaging Views # ============================================================================ class BulkMessageView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View): """ Bulk messaging interface. Features: - Select recipients (all, by tags, custom list) - Use template or custom message - Preview before sending - Track progress """ allowed_roles = [User.Role.ADMIN, User.Role.FRONT_DESK] def get(self, request): """Display bulk message form.""" form = BulkMessageForm(tenant=request.user.tenant) return render(request, 'notifications/bulk_message.html', {'form': form}) def post(self, request): """Process bulk message.""" from integrations.messaging_service import MessagingService form = BulkMessageForm(request.POST, tenant=request.user.tenant) if form.is_valid(): # Get recipients recipients = self._get_recipients(form.cleaned_data) if not recipients: messages.error(request, 'No recipients found.') return render(request, 'notifications/bulk_message.html', {'form': form}) # Get message content channel = form.cleaned_data['channel'] use_template = form.cleaned_data['use_template'] if use_template: template = form.cleaned_data['template'] # TODO: Implement template-based bulk sending messages.info(request, 'Template-based bulk sending not yet implemented.') else: message_body = form.cleaned_data['message'] # Send bulk messages service = MessagingService() result = service.send_bulk_messages( recipients=recipients, message=message_body, channel=channel, tenant_id=str(request.user.tenant.id) ) messages.success( request, f'Bulk send completed! Sent: {result["sent"]}, Failed: {result["failed"]}' ) return redirect('notifications:message_list') return render(request, 'notifications/bulk_message.html', {'form': form}) def _get_recipients(self, data): """Get list of recipients based on filter.""" recipient_filter = data['recipient_filter'] if recipient_filter == 'all': # Get all patients with phone numbers patients = Patient.objects.filter( tenant=self.request.user.tenant, phone__isnull=False ).exclude(phone='') return [p.phone for p in patients] elif recipient_filter == 'tags': # Get patients by tags tags = [t.strip() for t in data['tags'].split(',')] patients = Patient.objects.filter( tenant=self.request.user.tenant, tags__name__in=tags, phone__isnull=False ).exclude(phone='').distinct() return [p.phone for p in patients] elif recipient_filter == 'custom': # Parse custom recipient list recipients = data['recipients'].strip().split('\n') return [r.strip() for r in recipients if r.strip()] return [] # ============================================================================ # Analytics Views # ============================================================================ class MessageAnalyticsView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View): """ Message analytics and reports. Features: - Delivery rate charts - Channel comparison - Cost analysis - Trend analysis """ allowed_roles = [User.Role.ADMIN] def get(self, request): """Display analytics dashboard.""" tenant = request.user.tenant # Get date range days = int(request.GET.get('days', 30)) since = timezone.now() - timedelta(days=days) # Get analytics data analytics = self._get_analytics(tenant, since, days) context = { 'analytics': analytics, 'days': days, } return render(request, 'notifications/analytics.html', context) def _get_analytics(self, tenant, since, days): """Calculate analytics data.""" messages_qs = Message.objects.filter(tenant=tenant, created_at__gte=since) # Overall statistics total = messages_qs.count() successful = messages_qs.filter( status__in=[Message.Status.DELIVERED, Message.Status.READ] ).count() # Channel breakdown channel_stats = [] for channel in Message.Channel: channel_messages = messages_qs.filter(channel=channel.value) channel_total = channel_messages.count() channel_success = channel_messages.filter( status__in=[Message.Status.DELIVERED, Message.Status.READ] ).count() channel_stats.append({ 'channel': channel.label, 'total': channel_total, 'successful': channel_success, 'success_rate': round((channel_success / channel_total * 100), 1) if channel_total > 0 else 0 }) # Daily trend daily_trend = [] for i in range(days): date = timezone.now().date() - timedelta(days=days-1-i) day_messages = messages_qs.filter(created_at__date=date) daily_trend.append({ 'date': date.strftime('%Y-%m-%d'), 'total': day_messages.count(), 'successful': day_messages.filter( status__in=[Message.Status.DELIVERED, Message.Status.READ] ).count(), }) # Top templates top_templates = MessageTemplate.objects.filter( tenant=tenant, messages__created_at__gte=since ).annotate( usage_count=Count('messages') ).order_by('-usage_count')[:5] return { 'total': total, 'successful': successful, 'success_rate': round((successful / total * 100), 1) if total > 0 else 0, 'channel_stats': channel_stats, 'daily_trend': daily_trend, 'top_templates': top_templates, }