254 lines
8.1 KiB
Python
254 lines
8.1 KiB
Python
"""
|
|
Analytics & Reporting Views for IGNY8 Admin
|
|
"""
|
|
from django.contrib.admin.views.decorators import staff_member_required
|
|
from django.shortcuts import render
|
|
from django.db.models import Count, Sum, Avg, Q
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
import json
|
|
|
|
|
|
@staff_member_required
|
|
def revenue_report(request):
|
|
"""Revenue and billing analytics"""
|
|
from igny8_core.business.billing.models import Payment
|
|
from igny8_core.auth.models import Plan
|
|
|
|
# Date ranges
|
|
today = timezone.now()
|
|
months = []
|
|
monthly_revenue = []
|
|
|
|
for i in range(6):
|
|
month_start = today.replace(day=1) - timedelta(days=30*i)
|
|
month_end = month_start.replace(day=28) + timedelta(days=4)
|
|
|
|
revenue = Payment.objects.filter(
|
|
status='succeeded',
|
|
processed_at__gte=month_start,
|
|
processed_at__lt=month_end
|
|
).aggregate(total=Sum('amount'))['total'] or 0
|
|
|
|
months.insert(0, month_start.strftime('%b %Y'))
|
|
monthly_revenue.insert(0, float(revenue))
|
|
|
|
# Plan distribution
|
|
plan_distribution = Plan.objects.annotate(
|
|
account_count=Count('account')
|
|
).values('name', 'account_count')
|
|
|
|
# Payment method breakdown
|
|
payment_methods = Payment.objects.filter(
|
|
status='succeeded'
|
|
).values('payment_method').annotate(
|
|
count=Count('id'),
|
|
total=Sum('amount')
|
|
).order_by('-total')
|
|
|
|
# Total revenue all time
|
|
total_revenue = Payment.objects.filter(
|
|
status='succeeded'
|
|
).aggregate(total=Sum('amount'))['total'] or 0
|
|
|
|
context = {
|
|
'title': 'Revenue Report',
|
|
'months': json.dumps(months),
|
|
'monthly_revenue': json.dumps(monthly_revenue),
|
|
'plan_distribution': list(plan_distribution),
|
|
'payment_methods': list(payment_methods),
|
|
'total_revenue': float(total_revenue),
|
|
}
|
|
|
|
# Merge with admin context
|
|
from igny8_core.admin.site import admin_site
|
|
admin_context = admin_site.each_context(request)
|
|
context.update(admin_context)
|
|
|
|
return render(request, 'admin/reports/revenue.html', context)
|
|
|
|
|
|
@staff_member_required
|
|
def usage_report(request):
|
|
"""Credit usage and AI operations analytics"""
|
|
from igny8_core.business.billing.models import CreditUsageLog
|
|
|
|
# Usage by operation type
|
|
usage_by_operation = CreditUsageLog.objects.values(
|
|
'operation_type'
|
|
).annotate(
|
|
total_credits=Sum('credits_used'),
|
|
total_cost=Sum('cost_usd'),
|
|
operation_count=Count('id')
|
|
).order_by('-total_credits')
|
|
|
|
# Top credit consumers
|
|
top_consumers = CreditUsageLog.objects.values(
|
|
'account__name'
|
|
).annotate(
|
|
total_credits=Sum('credits_used'),
|
|
operation_count=Count('id')
|
|
).order_by('-total_credits')[:10]
|
|
|
|
# Model usage distribution
|
|
model_usage = CreditUsageLog.objects.values(
|
|
'model_used'
|
|
).annotate(
|
|
usage_count=Count('id')
|
|
).order_by('-usage_count')
|
|
|
|
# Total credits used
|
|
total_credits = CreditUsageLog.objects.aggregate(
|
|
total=Sum('credits_used')
|
|
)['total'] or 0
|
|
|
|
context = {
|
|
'title': 'Usage Report',
|
|
'usage_by_operation': list(usage_by_operation),
|
|
'top_consumers': list(top_consumers),
|
|
'model_usage': list(model_usage),
|
|
'total_credits': int(total_credits),
|
|
}
|
|
|
|
# Merge with admin context
|
|
from igny8_core.admin.site import admin_site
|
|
admin_context = admin_site.each_context(request)
|
|
context.update(admin_context)
|
|
|
|
return render(request, 'admin/reports/usage.html', context)
|
|
|
|
|
|
@staff_member_required
|
|
def content_report(request):
|
|
"""Content production analytics"""
|
|
from igny8_core.modules.writer.models import Content, Tasks
|
|
|
|
# Content by type
|
|
content_by_type = Content.objects.values(
|
|
'content_type'
|
|
).annotate(count=Count('id')).order_by('-count')
|
|
|
|
# Production timeline (last 30 days)
|
|
days = []
|
|
daily_counts = []
|
|
for i in range(30):
|
|
day = timezone.now().date() - timedelta(days=i)
|
|
count = Content.objects.filter(created_at__date=day).count()
|
|
days.insert(0, day.strftime('%m/%d'))
|
|
daily_counts.insert(0, count)
|
|
|
|
# Average word count by content type
|
|
avg_words = Content.objects.values('content_type').annotate(
|
|
avg_words=Avg('word_count')
|
|
).order_by('-avg_words')
|
|
|
|
# Task completion rate
|
|
total_tasks = Tasks.objects.count()
|
|
completed_tasks = Tasks.objects.filter(status='completed').count()
|
|
completion_rate = (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0
|
|
|
|
# Total content produced
|
|
total_content = Content.objects.count()
|
|
|
|
context = {
|
|
'title': 'Content Production Report',
|
|
'content_by_type': list(content_by_type),
|
|
'days': json.dumps(days),
|
|
'daily_counts': json.dumps(daily_counts),
|
|
'avg_words': list(avg_words),
|
|
'completion_rate': round(completion_rate, 1),
|
|
'total_content': total_content,
|
|
'total_tasks': total_tasks,
|
|
'completed_tasks': completed_tasks,
|
|
}
|
|
|
|
# Merge with admin context
|
|
from igny8_core.admin.site import admin_site
|
|
admin_context = admin_site.each_context(request)
|
|
context.update(admin_context)
|
|
|
|
return render(request, 'admin/reports/content.html', context)
|
|
|
|
|
|
@staff_member_required
|
|
def data_quality_report(request):
|
|
"""Check data quality and integrity"""
|
|
issues = []
|
|
|
|
# Orphaned content (no site)
|
|
from igny8_core.modules.writer.models import Content
|
|
orphaned_content = Content.objects.filter(site__isnull=True).count()
|
|
if orphaned_content > 0:
|
|
issues.append({
|
|
'severity': 'warning',
|
|
'type': 'Orphaned Records',
|
|
'count': orphaned_content,
|
|
'description': 'Content items without assigned site',
|
|
'action_url': '/admin/writer/content/?site__isnull=True'
|
|
})
|
|
|
|
# Tasks without clusters
|
|
from igny8_core.modules.writer.models import Tasks
|
|
tasks_no_cluster = Tasks.objects.filter(cluster__isnull=True).count()
|
|
if tasks_no_cluster > 0:
|
|
issues.append({
|
|
'severity': 'info',
|
|
'type': 'Missing Relationships',
|
|
'count': tasks_no_cluster,
|
|
'description': 'Tasks without assigned cluster',
|
|
'action_url': '/admin/writer/tasks/?cluster__isnull=True'
|
|
})
|
|
|
|
# Accounts with negative credits
|
|
from igny8_core.auth.models import Account
|
|
negative_credits = Account.objects.filter(credits__lt=0).count()
|
|
if negative_credits > 0:
|
|
issues.append({
|
|
'severity': 'error',
|
|
'type': 'Data Integrity',
|
|
'count': negative_credits,
|
|
'description': 'Accounts with negative credit balance',
|
|
'action_url': '/admin/igny8_core_auth/account/?credits__lt=0'
|
|
})
|
|
|
|
# Duplicate keywords
|
|
from igny8_core.modules.planner.models import Keywords
|
|
duplicates = Keywords.objects.values('keyword', 'site', 'sector').annotate(
|
|
count=Count('id')
|
|
).filter(count__gt=1).count()
|
|
if duplicates > 0:
|
|
issues.append({
|
|
'severity': 'warning',
|
|
'type': 'Duplicates',
|
|
'count': duplicates,
|
|
'description': 'Duplicate keywords for same site/sector',
|
|
'action_url': '/admin/planner/keywords/'
|
|
})
|
|
|
|
# Content without SEO data
|
|
no_seo = Content.objects.filter(
|
|
Q(meta_title__isnull=True) | Q(meta_title='') |
|
|
Q(meta_description__isnull=True) | Q(meta_description='')
|
|
).count()
|
|
if no_seo > 0:
|
|
issues.append({
|
|
'severity': 'info',
|
|
'type': 'Incomplete Data',
|
|
'count': no_seo,
|
|
'description': 'Content missing SEO metadata',
|
|
'action_url': '/admin/writer/content/'
|
|
})
|
|
|
|
context = {
|
|
'title': 'Data Quality Report',
|
|
'issues': issues,
|
|
'total_issues': len(issues),
|
|
}
|
|
|
|
# Merge with admin context
|
|
from igny8_core.admin.site import admin_site
|
|
admin_context = admin_site.each_context(request)
|
|
context.update(admin_context)
|
|
|
|
return render(request, 'admin/reports/data_quality.html', context)
|