django phase 3 and 4

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-15 00:08:18 +00:00
parent aa48a55504
commit cda56f15ba
14 changed files with 792 additions and 887 deletions

View File

@@ -66,8 +66,8 @@ class Igny8AdminConfig(AdminConfig):
def _setup_celery_admin(self):
"""Setup enhanced Celery admin with proper unregister/register"""
try:
from django_celery_results.models import TaskResult
from igny8_core.admin.celery_admin import CeleryTaskResultAdmin
from django_celery_results.models import TaskResult, GroupResult
from igny8_core.admin.celery_admin import CeleryTaskResultAdmin, CeleryGroupResultAdmin
# Unregister the default TaskResult admin
try:
@@ -75,8 +75,15 @@ class Igny8AdminConfig(AdminConfig):
except admin.sites.NotRegistered:
pass
# Register our enhanced version
# Unregister the default GroupResult admin
try:
admin.site.unregister(GroupResult)
except admin.sites.NotRegistered:
pass
# Register our enhanced versions
admin.site.register(TaskResult, CeleryTaskResultAdmin)
admin.site.register(GroupResult, CeleryGroupResultAdmin)
except Exception as e:
# Log the error but don't crash the app
import logging

View File

@@ -4,9 +4,10 @@ Celery Task Monitoring Admin - Unfold Style
from django.contrib import admin
from django.utils.html import format_html
from django.contrib import messages
from django_celery_results.models import TaskResult
from django_celery_results.models import TaskResult, GroupResult
from unfold.admin import ModelAdmin
from unfold.contrib.filters.admin import RangeDateFilter
from celery import current_app
class CeleryTaskResultAdmin(ModelAdmin):
@@ -79,12 +80,15 @@ class CeleryTaskResultAdmin(ModelAdmin):
seconds = duration.total_seconds()
if seconds < 1:
return format_html('<span style="color: #0bbf87;">{:.2f}ms</span>', seconds * 1000)
time_str = f'{seconds * 1000:.2f}ms'
return format_html('<span style="color: #0bbf87;">{}</span>', time_str)
elif seconds < 60:
return format_html('<span style="color: #0693e3;">{:.2f}s</span>', seconds)
time_str = f'{seconds:.2f}s'
return format_html('<span style="color: #0693e3;">{}</span>', time_str)
else:
minutes = seconds / 60
return format_html('<span style="color: #ff7a00;">{:.1f}m</span>', minutes)
time_str = f'{minutes:.1f}m'
return format_html('<span style="color: #ff7a00;">{}</span>', time_str)
return '-'
execution_time.short_description = 'Duration'
@@ -143,9 +147,9 @@ class CeleryTaskResultAdmin(ModelAdmin):
count = old_tasks.count()
old_tasks.delete()
self.message_user(request, f'🗑️ Cleared {count} old task(s)', messages.SUCCESS)
self.message_user(request, f'Cleared {count} old task(s)', messages.SUCCESS)
clear_old_tasks.short_description = '🗑️ Clear Old Tasks (30+ days)'
clear_old_tasks.short_description = 'Clear Old Tasks (30+ days)'
def has_add_permission(self, request):
"""Disable manual task creation"""
@@ -154,3 +158,56 @@ class CeleryTaskResultAdmin(ModelAdmin):
def has_change_permission(self, request, obj=None):
"""Make read-only"""
return False
class CeleryGroupResultAdmin(ModelAdmin):
"""Admin interface for monitoring Celery group results with Unfold styling"""
list_display = [
'group_id',
'date_created',
'date_done',
'result_count',
]
list_filter = [
('date_created', RangeDateFilter),
('date_done', RangeDateFilter),
]
search_fields = ['group_id', 'result']
readonly_fields = [
'group_id', 'date_created', 'date_done', 'content_type',
'content_encoding', 'result'
]
date_hierarchy = 'date_created'
ordering = ['-date_created']
fieldsets = (
('Group Information', {
'fields': ('group_id', 'date_created', 'date_done')
}),
('Result Details', {
'fields': ('content_type', 'content_encoding', 'result'),
'classes': ('collapse',)
}),
)
def result_count(self, obj):
"""Count tasks in the group"""
if obj.result:
try:
import json
result_data = json.loads(obj.result) if isinstance(obj.result, str) else obj.result
if isinstance(result_data, list):
return len(result_data)
except:
pass
return '-'
result_count.short_description = 'Task Count'
def has_add_permission(self, request):
"""Disable manual group result creation"""
return False
def has_change_permission(self, request, obj=None):
"""Make read-only"""
return False

View File

@@ -0,0 +1,253 @@
"""
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)

View File

@@ -21,16 +21,26 @@ class Igny8AdminSite(UnfoldAdminSite):
index_title = 'IGNY8 Administration'
def get_urls(self):
"""Get admin URLs with dashboard available at /admin/dashboard/"""
"""Get admin URLs with dashboard and reports available"""
from django.urls import path
from .dashboard import admin_dashboard
from .reports import revenue_report, usage_report, content_report, data_quality_report
urls = super().get_urls()
custom_urls = [
path('dashboard/', self.admin_view(admin_dashboard), name='dashboard'),
path('reports/revenue/', self.admin_view(revenue_report), name='report_revenue'),
path('reports/usage/', self.admin_view(usage_report), name='report_usage'),
path('reports/content/', self.admin_view(content_report), name='report_content'),
path('reports/data-quality/', self.admin_view(data_quality_report), name='report_data_quality'),
]
return custom_urls + urls
def index(self, request, extra_context=None):
"""Redirect to custom dashboard"""
from django.shortcuts import redirect
return redirect('admin:dashboard')
def get_sidebar_list(self, request):
"""
Override Unfold's get_sidebar_list to return our custom app groups

View File

@@ -0,0 +1,117 @@
{% extends "admin/base_site.html" %}
{% load static %}
{% block content %}
<div class="px-6 py-4">
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Content Production Report</h1>
<p class="text-gray-600 dark:text-gray-400">Content creation metrics and task completion analytics</p>
</div>
<!-- Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Total Content</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ total_content }}</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Total Tasks</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ total_tasks }}</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Completed</h3>
<p class="text-3xl font-bold text-green-600">{{ completed_tasks }}</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Completion Rate</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ completion_rate }}%</p>
</div>
</div>
<!-- Production Timeline -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Daily Production (Last 30 Days)</h2>
<canvas id="productionChart" height="80"></canvas>
</div>
<!-- Content by Type -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Content by Type</h2>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Content Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Count</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
{% for content in content_by_type %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ content.content_type|default:"Unknown" }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ content.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Average Word Count -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Average Word Count by Type</h2>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Content Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Avg Words</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
{% for avg in avg_words %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ avg.content_type|default:"Unknown" }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ avg.avg_words|floatformat:0 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
const ctx = document.getElementById('productionChart');
new Chart(ctx, {
type: 'bar',
data: {
labels: {{ days|safe }},
datasets: [{
label: 'Content Produced',
data: {{ daily_counts|safe }},
backgroundColor: '#0bbf87',
borderColor: '#0bbf87',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: true,
position: 'top',
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,91 @@
{% extends "admin/base_site.html" %}
{% load static %}
{% block content %}
<div class="px-6 py-4">
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Data Quality Report</h1>
<p class="text-gray-600 dark:text-gray-400">System integrity and data quality checks</p>
</div>
<!-- Summary Card -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Total Issues Found</h3>
<p class="text-4xl font-bold {% if total_issues == 0 %}text-green-600{% elif total_issues < 5 %}text-yellow-600{% else %}text-red-600{% endif %}">
{{ total_issues }}
</p>
</div>
{% if total_issues == 0 %}
<div class="text-green-600">
<svg class="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
{% endif %}
</div>
</div>
<!-- Issues List -->
{% if issues %}
<div class="space-y-4">
{% for issue in issues %}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<div class="flex items-center">
{% if issue.severity == 'error' %}
<div class="flex-shrink-0 w-12 h-12 bg-red-100 dark:bg-red-900 rounded-lg flex items-center justify-center mr-4">
<svg class="w-6 h-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
</div>
{% elif issue.severity == 'warning' %}
<div class="flex-shrink-0 w-12 h-12 bg-yellow-100 dark:bg-yellow-900 rounded-lg flex items-center justify-center mr-4">
<svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
</div>
{% else %}
<div class="flex-shrink-0 w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center mr-4">
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
{% endif %}
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ issue.type }}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">{{ issue.description }}</p>
</div>
</div>
<div class="flex items-center">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {% if issue.severity == 'error' %}bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200{% elif issue.severity == 'warning' %}bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200{% else %}bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200{% endif %}">
{{ issue.count }} issue{{ issue.count|pluralize }}
</span>
</div>
</div>
<div class="flex justify-end">
<a href="{{ issue.action_url }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Review & Fix
<svg class="ml-2 -mr-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg p-8 text-center">
<svg class="mx-auto w-16 h-16 text-green-600 dark:text-green-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 class="text-xl font-semibold text-green-900 dark:text-green-100 mb-2">All Clear!</h3>
<p class="text-green-700 dark:text-green-300">No data quality issues found. Your system is healthy.</p>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,115 @@
{% extends "admin/base_site.html" %}
{% load static %}
{% block content %}
<div class="px-6 py-4">
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Revenue Report</h1>
<p class="text-gray-600 dark:text-gray-400">Financial performance and billing analytics</p>
</div>
<!-- Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Total Revenue</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white">${{ total_revenue|floatformat:2 }}</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Payment Methods</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ payment_methods|length }}</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Active Plans</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ plan_distribution|length }}</p>
</div>
</div>
<!-- Revenue Chart -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Monthly Revenue (Last 6 Months)</h2>
<canvas id="revenueChart" height="80"></canvas>
</div>
<!-- Plan Distribution -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Plan Distribution</h2>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Plan Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Accounts</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
{% for plan in plan_distribution %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ plan.name }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ plan.account_count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Payment Methods -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Payment Methods</h2>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Method</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Count</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Total</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
{% for method in payment_methods %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ method.payment_method|default:"Unknown" }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ method.count }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">${{ method.total|floatformat:2 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
const ctx = document.getElementById('revenueChart');
new Chart(ctx, {
type: 'line',
data: {
labels: {{ months|safe }},
datasets: [{
label: 'Revenue ($)',
data: {{ monthly_revenue|safe }},
borderColor: '#0693e3',
backgroundColor: 'rgba(6, 147, 227, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: true,
position: 'top',
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,103 @@
{% extends "admin/base_site.html" %}
{% load static %}
{% block content %}
<div class="px-6 py-4">
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Usage Report</h1>
<p class="text-gray-600 dark:text-gray-400">Credit usage and AI operations analytics</p>
</div>
<!-- Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Total Credits Used</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ total_credits|floatformat:0 }}</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Operation Types</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ usage_by_operation|length }}</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Active Accounts</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ top_consumers|length }}</p>
</div>
</div>
<!-- Usage by Operation Type -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Usage by Operation Type</h2>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Operation</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Credits Used</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Cost (USD)</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Operations</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
{% for usage in usage_by_operation %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ usage.operation_type }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ usage.total_credits|floatformat:0 }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">${{ usage.total_cost|floatformat:2 }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ usage.operation_count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Top Credit Consumers -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Top Credit Consumers</h2>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Account</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Total Credits</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Operations</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
{% for consumer in top_consumers %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ consumer.account__name }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ consumer.total_credits|floatformat:0 }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ consumer.operation_count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Model Usage Distribution -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Model Usage Distribution</h2>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Model</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Usage Count</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
{% for model in model_usage %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ model.model_used }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ model.usage_count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}