894 lines
31 KiB
Python
894 lines
31 KiB
Python
"""
|
|
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,
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# Notification Center Views (In-App Notifications)
|
|
# ============================================================================
|
|
|
|
|
|
class NotificationListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
List all notifications for the current user.
|
|
|
|
Features:
|
|
- Show unread notifications first
|
|
- Filter by type
|
|
- Mark as read/unread
|
|
- Pagination
|
|
"""
|
|
model = None # Will be set in get_queryset
|
|
template_name = 'notifications/notification_list.html'
|
|
context_object_name = 'notifications'
|
|
paginate_by = 20
|
|
|
|
def get_queryset(self):
|
|
"""Get notifications for current user."""
|
|
from .models import Notification
|
|
|
|
queryset = Notification.objects.filter(user=self.request.user)
|
|
|
|
# Filter by read status
|
|
filter_type = self.request.GET.get('filter', 'all')
|
|
if filter_type == 'unread':
|
|
queryset = queryset.filter(is_read=False)
|
|
elif filter_type == 'read':
|
|
queryset = queryset.filter(is_read=True)
|
|
|
|
# Filter by notification type
|
|
notif_type = self.request.GET.get('type')
|
|
if notif_type:
|
|
queryset = queryset.filter(notification_type=notif_type)
|
|
|
|
return queryset.order_by('-created_at')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add unread count and filter info."""
|
|
context = super().get_context_data(**kwargs)
|
|
from .models import Notification
|
|
|
|
context['unread_count'] = Notification.get_unread_count(self.request.user)
|
|
context['current_filter'] = self.request.GET.get('filter', 'all')
|
|
context['current_type'] = self.request.GET.get('type', '')
|
|
|
|
return context
|
|
|
|
|
|
class NotificationMarkReadView(LoginRequiredMixin, View):
|
|
"""Mark a notification as read."""
|
|
|
|
def post(self, request, pk):
|
|
"""Mark notification as read."""
|
|
from .models import Notification
|
|
|
|
notification = get_object_or_404(Notification, pk=pk, user=request.user)
|
|
notification.mark_as_read()
|
|
|
|
# Return JSON for AJAX requests
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
|
return JsonResponse({
|
|
'success': True,
|
|
'unread_count': Notification.get_unread_count(request.user)
|
|
})
|
|
|
|
# Redirect to next URL or notification list
|
|
next_url = request.GET.get('next', 'notifications:notification_list')
|
|
return redirect(next_url)
|
|
|
|
|
|
class NotificationMarkAllReadView(LoginRequiredMixin, View):
|
|
"""Mark all notifications as read for current user."""
|
|
|
|
def post(self, request):
|
|
"""Mark all notifications as read."""
|
|
from .models import Notification
|
|
|
|
Notification.mark_all_as_read(request.user)
|
|
|
|
# Return JSON for AJAX requests
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
|
return JsonResponse({
|
|
'success': True,
|
|
'unread_count': 0
|
|
})
|
|
|
|
messages.success(request, 'All notifications marked as read.')
|
|
return redirect('notifications:notification_list')
|
|
|
|
|
|
class NotificationUnreadCountView(LoginRequiredMixin, View):
|
|
"""Get unread notification count (for AJAX polling)."""
|
|
|
|
def get(self, request):
|
|
"""Return unread count as JSON."""
|
|
from .models import Notification
|
|
|
|
unread_count = Notification.get_unread_count(request.user)
|
|
|
|
return JsonResponse({
|
|
'unread_count': unread_count
|
|
})
|
|
|
|
|
|
class NotificationDropdownView(LoginRequiredMixin, View):
|
|
"""Get recent notifications for dropdown (AJAX)."""
|
|
|
|
def get(self, request):
|
|
"""Return recent notifications as JSON."""
|
|
from .models import Notification
|
|
|
|
notifications = Notification.objects.filter(
|
|
user=request.user
|
|
).order_by('-created_at')[:10]
|
|
|
|
data = {
|
|
'unread_count': Notification.get_unread_count(request.user),
|
|
'notifications': [
|
|
{
|
|
'id': str(n.id),
|
|
'title': n.title,
|
|
'message': n.message[:100] + '...' if len(n.message) > 100 else n.message,
|
|
'type': n.notification_type,
|
|
'is_read': n.is_read,
|
|
'created_at': n.created_at.isoformat(),
|
|
'action_url': n.action_url or '#',
|
|
}
|
|
for n in notifications
|
|
]
|
|
}
|
|
|
|
return JsonResponse(data)
|