django phase 3 and 4
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
253
backend/igny8_core/admin/reports.py
Normal file
253
backend/igny8_core/admin/reports.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
117
backend/igny8_core/templates/admin/reports/content.html
Normal file
117
backend/igny8_core/templates/admin/reports/content.html
Normal 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 %}
|
||||
91
backend/igny8_core/templates/admin/reports/data_quality.html
Normal file
91
backend/igny8_core/templates/admin/reports/data_quality.html
Normal 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 %}
|
||||
115
backend/igny8_core/templates/admin/reports/revenue.html
Normal file
115
backend/igny8_core/templates/admin/reports/revenue.html
Normal 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 %}
|
||||
103
backend/igny8_core/templates/admin/reports/usage.html
Normal file
103
backend/igny8_core/templates/admin/reports/usage.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user