@ -11,6 +11,7 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import RedirectView
from django.views.generic import RedirectView
from django.utils.translation import get_language
from django.utils.translation import get_language
from django.http import JsonResponse
from django.http import JsonResponse
from django.utils.html import format_html
from django.views.decorators.http import require_POST
from django.views.decorators.http import require_POST
# --- AGGRESSIVE UNICODE PATCH FOR UNFOLD 0.64.1 TABS ---
# --- AGGRESSIVE UNICODE PATCH FOR UNFOLD 0.64.1 TABS ---
@ -119,10 +120,10 @@ class LoginForm:
class FormulaAdminSite ( UnfoldAdminSite ) :
class FormulaAdminSite ( UnfoldAdminSite ) :
""" Main Admin for Imam Jawad """
""" Main Admin for Imam Jawad """
site_header = " Imam Jawad Admin "
site_title = " Imam Jawad Admin "
index_title = " System Administration "
site_subheader = " Imam Jawad School "
site_header = _ ( " Imam Javad Admin " )
site_title = _ ( " Imam Javad Admin " )
index_title = _ ( " System Administration " )
site_subheader = _ ( " Imam Javad School " )
def __init__ ( self , * args , * * kwargs ) :
def __init__ ( self , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
super ( ) . __init__ ( * args , * * kwargs )
@ -174,9 +175,18 @@ class FormulaAdminSite(UnfoldAdminSite):
" 500 " : " 107 114 128 " ,
" 500 " : " 107 114 128 " ,
" 600 " : " 75 85 99 " ,
" 600 " : " 75 85 99 " ,
" 700 " : " 55 65 81 " ,
" 700 " : " 55 65 81 " ,
" 800 " : " 31 41 55 " ,
" 900 " : " 17 24 39 " ,
" 950 " : " 3 7 18 " ,
# "800": "31 41 55",
# "900": "17 24 39",
# "950": "3 7 18",
# "800": "12 19 26", # #0C131A (Cards / Sidebar)
# "900": "1 31 34", # #011F22 (Main Background)
# "950": "1 19 21", # #011315 (Deep Accents)
# "800": "2 22 24", # Cards / Sidebar (Slightly elevated dark green)
# "900": "0 12 14", # Main Background (Almost pitch black-green)
# "950": "0 5 6", # Deepest Accents (Effectively black)
" 800 " : " 10 36 38 " , # Cards (Lighter to float above the background)
" 900 " : " 3 21 22 " , # Main Background (Lighter dark-green)
" 950 " : " 0 5 6 " , # Sidebar (Kept exactly the same!)
} ,
} ,
" primary " : {
" primary " : {
" 50 " : " 234 253 243 " ,
" 50 " : " 234 253 243 " ,
@ -217,10 +227,10 @@ class FormulaAdminSite(UnfoldAdminSite):
class DovoodiAdminSite ( UnfoldAdminSite ) :
class DovoodiAdminSite ( UnfoldAdminSite ) :
""" Secondary Admin for Dovoodi """
""" Secondary Admin for Dovoodi """
site_header = " Dovoodi Admin "
site_title = " Dovoodi Admin "
index_title = " System Administration "
site_subheader = " Dovodbi Application "
site_header = _ ( " Dovoodi Admin " )
site_title = _ ( " Dovoodi Admin " )
index_title = _ ( " System Administration " )
site_subheader = _ ( " Dovodbi Application " )
def __init__ ( self , * args , * * kwargs ) :
def __init__ ( self , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
super ( ) . __init__ ( * args , * * kwargs )
@ -449,18 +459,18 @@ class HomeView(RedirectView):
# ---------------------------------------------------------
# ---------------------------------------------------------
def dashboard_callback ( request , context ) :
def dashboard_callback ( request , context ) :
"""
Generates dynamic KPI cards for the custom dashboard .
Shows different stats depending on whether it ' s Dovoodi or Imam Javad.
"""
from django.apps import apps
from django.apps import apps
from django.db.models import Count , Sum
from django.utils import timezone
if context is None :
if context is None :
context = { }
context = { }
context . update ( {
context . update ( {
" navigation " : [ { " title " : _ ( " Dashboard " ) , " link " : " / " , " active " : True } ] ,
" navigation " : [ { " title " : _ ( " Dashboard " ) , " link " : " / " , " active " : True } ] ,
" kpi " : [ ]
" kpi " : [ ] ,
" top_courses " : [ ] ,
" tx_stats " : { } , # New dict for our multi-segment donut chart
} )
} )
if not hasattr ( request , " user " ) or not request . user . is_authenticated :
if not hasattr ( request , " user " ) or not request . user . is_authenticated :
@ -472,34 +482,82 @@ def dashboard_callback(request, context):
if is_main_panel ( request ) :
if is_main_panel ( request ) :
try :
try :
StudentUser = apps . get_model ( ' account ' , ' StudentUser ' )
StudentUser = apps . get_model ( ' account ' , ' StudentUser ' )
ProfessorUser = apps . get_model ( ' account ' , ' ProfessorUser ' )
Course = apps . get_model ( ' course ' , ' Course ' )
Course = apps . get_model ( ' course ' , ' Course ' )
Blog = apps . get_model ( ' blog ' , ' Blog ' )
Certificate = apps . get_model ( ' certificate ' , ' Certificate ' )
Certificate = apps . get_model ( ' certificate ' , ' Certificate ' )
Transaction = apps . get_model ( ' transaction ' , ' TransactionParticipant ' )
# --- 1. Basic Counts ---
active_students = StudentUser . objects . filter ( is_active = True ) . count ( )
active_professors = ProfessorUser . objects . filter ( is_active = True ) . count ( )
active_courses = Course . objects . exclude ( status = ' inactive ' ) . count ( )
total_blogs = Blog . objects . count ( )
# Certificates logic respecting permissions
# --- 2. Certificates ---
certs_qs = Certificate . objects . filter ( status = ' pending ' )
certs_qs = Certificate . objects . filter ( status = ' pending ' )
if not request . user . is_staff and not getattr ( request . user , ' is_superuser ' , False ) :
if not request . user . is_staff and not getattr ( request . user , ' is_superuser ' , False ) :
certs_qs = certs_qs . filter ( course__professor = request . user )
certs_qs = certs_qs . filter ( course__professor = request . user )
pending_certs = certs_qs . count ( )
pending_certs = certs_qs . count ( )
cert_footer_text = " Action Required " if pending_certs > 0 else " All Caught Up "
cert_footer_color = " text-orange-500 " if pending_certs > 0 else " text-green-500 "
# --- 3. Revenue (Last 30 Days) ---
thirty_days_ago = timezone . now ( ) - timezone . timedelta ( days = 30 )
revenue_data = Transaction . objects . filter (
status = ' success ' ,
created_at__gte = thirty_days_ago
) . aggregate ( Sum ( ' price ' ) )
revenue = revenue_data [ ' price__sum ' ] or 0
# --- 4. Transaction Multi-Status Breakdown ---
total_tx = Transaction . objects . count ( )
if total_tx > 0 :
success_count = Transaction . objects . filter ( status = ' success ' ) . count ( )
pending_count = Transaction . objects . filter ( status = ' pending ' ) . count ( )
waiting_count = Transaction . objects . filter ( status = ' waiting_approval ' ) . count ( )
failed_count = Transaction . objects . filter ( status = ' failed ' ) . count ( )
# Calculate percentages
pct_success = ( success_count / total_tx ) * 100
pct_pending = ( pending_count / total_tx ) * 100
pct_waiting = ( waiting_count / total_tx ) * 100
pct_failed = ( failed_count / total_tx ) * 100
# Calculate SVG Dash Offsets (Accumulative for overlapping circles)
offset_success = 100 - pct_success
offset_pending = 100 - ( pct_success + pct_pending )
offset_waiting = 100 - ( pct_success + pct_pending + pct_waiting )
offset_failed = 100 - ( pct_success + pct_pending + pct_waiting + pct_failed ) # Should be 0
else :
pct_success = pct_pending = pct_waiting = pct_failed = 0
offset_success = offset_pending = offset_waiting = offset_failed = 100
context [ " tx_stats " ] = {
" total " : total_tx ,
" pct_success " : round ( pct_success , 1 ) ,
" pct_pending " : round ( pct_pending , 1 ) ,
" pct_waiting " : round ( pct_waiting , 1 ) ,
" pct_failed " : round ( pct_failed , 1 ) ,
# Format as strings to prevent Django from converting dots to commas in Russian
" offset_success " : f " {offset_success:.2f} " ,
" offset_pending " : f " {offset_pending:.2f} " ,
" offset_waiting " : f " {offset_waiting:.2f} " ,
}
# --- 5. Top 5 Courses ---
top_courses = Course . objects . select_related ( ' professor ' ) . annotate (
participant_count = Count ( ' participants ' )
) . order_by ( ' -participant_count ' ) [ : 5 ]
context [ " top_courses " ] = top_courses
# --- Map to KPIs ---
context [ " kpi " ] = [
context [ " kpi " ] = [
{
" title " : _ ( " Active Students " ) ,
" metric " : f " {StudentUser.objects.filter(is_active=True).count():,} " ,
" footer " : mark_safe ( ' <strong class= " text-green-500 font-medium " >Platform Users</strong> ' ) ,
} ,
{
" title " : _ ( " Published Courses " ) ,
" metric " : f " {Course.objects.exclude(status= ' inactive ' ).count():,} " ,
" footer " : mark_safe ( ' <strong class= " text-blue-500 font-medium " >Total Offerings</strong> ' ) ,
} ,
{
" title " : _ ( " Pending Certificates " ) ,
" metric " : f " {pending_certs:,} " ,
" footer " : mark_safe ( f ' <strong class= " {cert_footer_color} font-medium " >{cert_footer_text}</strong> ' ) ,
} ,
{ " title " : _ ( " Active Students " ) , " metric " : f " {active_students:,} " } ,
{ " title " : _ ( " Professors " ) , " metric " : f " {active_professors:,} " } ,
{ " title " : _ ( " Active Courses " ) , " metric " : f " {active_courses:,} " } ,
{ " title " : _ ( " Total Blogs " ) , " metric " : f " {total_blogs:,} " } ,
{ " title " : _ ( " 30-Day Revenue " ) , " metric " : f " ${revenue:,.2f} " , " footer " : format_html ( ' <strong class= " text-green-500 font-medium " >+ {}</strong> ' , _ ( " Updated Today " ) ) } ,
{ " title " : _ ( " Pending Certificates " ) , " metric " : f " {pending_certs:,} " , " footer " : format_html ( ' <strong class= " text-orange-500 font-medium " >{}</strong> ' , _ ( " Requires Action " ) ) if pending_certs > 0 else " " } ,
]
]
except Exception as e :
except Exception as e :
print ( f " Dashboard KPI Error (Main Panel): {e} " )
print ( f " Dashboard KPI Error (Main Panel): {e} " )
@ -509,7 +567,6 @@ def dashboard_callback(request, context):
# -------------------------------------------------------------
# -------------------------------------------------------------
else :
else :
try :
try :
# Safely fetch Dovoodi specific models
Video = apps . get_model ( ' video ' , ' Video ' )
Video = apps . get_model ( ' video ' , ' Video ' )
Book = apps . get_model ( ' library ' , ' Book ' )
Book = apps . get_model ( ' library ' , ' Book ' )
Article = apps . get_model ( ' article ' , ' Article ' )
Article = apps . get_model ( ' article ' , ' Article ' )
@ -520,21 +577,9 @@ def dashboard_callback(request, context):
total_reading = Book . objects . count ( ) + Article . objects . count ( )
total_reading = Book . objects . count ( ) + Article . objects . count ( )
context [ " kpi " ] = [
context [ " kpi " ] = [
{
" title " : _ ( " Hadith Database " ) ,
" metric " : f " {Hadis.objects.count():,} " ,
" footer " : mark_safe ( ' <strong class= " text-amber-500 font-medium " >Total Records</strong> ' ) ,
} ,
{
" title " : _ ( " Books & Articles " ) ,
" metric " : f " {total_reading:,} " ,
" footer " : mark_safe ( ' <strong class= " text-blue-500 font-medium " >Reading Materials</strong> ' ) ,
} ,
{
" title " : _ ( " Multimedia " ) ,
" metric " : f " {total_multimedia:,} " ,
" footer " : mark_safe ( ' <strong class= " text-purple-500 font-medium " >Videos & Podcasts</strong> ' ) ,
} ,
{ " title " : _ ( " Hadith Database " ) , " metric " : f " {Hadis.objects.count():,} " } ,
{ " title " : _ ( " Books & Articles " ) , " metric " : f " {total_reading:,} " } ,
{ " title " : _ ( " Multimedia " ) , " metric " : f " {total_multimedia:,} " } ,
]
]
except Exception as e :
except Exception as e :
print ( f " Dashboard KPI Error (Dovoodi Panel): {e} " )
print ( f " Dashboard KPI Error (Dovoodi Panel): {e} " )