Browse Source

dashboard and style enhancement

upgrade dashboard to a better version having different data cards and charts and lists , to observe better information

update the background color of admin panel synced with imam-jawad theme

update unfold translations with new dashboard values
master
Mohsen Taba 2 weeks ago
parent
commit
f8f69868a3
  1. 2
      apps/account/templates/account/user_list_section.html
  2. 137
      templates/admin/index.html
  3. 145
      utils/admin.py
  4. 148
      utils/unfold_translations.py

2
apps/account/templates/account/user_list_section.html

@ -5,7 +5,7 @@
<div class="grid gap-4 mb-4 md:grid-cols-2 lg:grid-cols-4 "> <div class="grid gap-4 mb-4 md:grid-cols-2 lg:grid-cols-4 ">
{% component "unfold/components/card.html" %} {% component "unfold/components/card.html" %}
{% component "unfold/components/text.html" %} {% component "unfold/components/text.html" %}
{% trans "Total Actice Users" %}
{% trans "Total Active Users" %}
{% endcomponent %} {% endcomponent %}
{% component "unfold/components/title.html" with component_class="AllUserComponent" %}{% endcomponent %} {% component "unfold/components/title.html" with component_class="AllUserComponent" %}{% endcomponent %}

137
templates/admin/index.html

@ -20,22 +20,27 @@
</h1> </h1>
</div> </div>
<div class="grid gap-4 mb-4 md:grid-cols-2 lg:grid-cols-3">
<!-- Top Row: KPI Cards using Unfold Native Components -->
<div class="grid gap-4 mb-8 md:grid-cols-2 lg:grid-cols-3">
{% for item in kpi %} {% for item in kpi %}
{% component "unfold/components/card.html" %} {% component "unfold/components/card.html" %}
{% component "unfold/components/text.html" %} {% component "unfold/components/text.html" %}
{{ item.title }} {{ item.title }}
{% endcomponent %} {% endcomponent %}
{% component "unfold/components/title.html" %}
{{ item.metric }}
{% endcomponent %}
<div class="mt-2">
{% component "unfold/components/title.html" %}
{{ item.metric }}
{% endcomponent %}
</div>
{% if item.footer %} {% if item.footer %}
<div class="mt-2 text-sm">
<div class="mt-4 text-sm">
{{ item.footer|safe }} {{ item.footer|safe }}
</div> </div>
{% endif %} {% endif %}
{% endcomponent %} {% endcomponent %}
{% empty %} {% empty %}
<p class="text-gray-500 dark:text-gray-400 col-span-full"> <p class="text-gray-500 dark:text-gray-400 col-span-full">
@ -43,4 +48,126 @@
</p> </p>
{% endfor %} {% endfor %}
</div> </div>
<!-- Bottom Row: Top Courses List & Donut Chart -->
{% if top_courses %}
<div class="grid gap-4 md:grid-cols-3 items-start">
<!-- Left Column: Top 5 Courses -->
<div class="md:col-span-2">
{% component "unfold/components/card.html" %}
<div class="border-b border-gray-200 dark:border-gray-700 pb-4 mb-2">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">{% trans "Top 5 Popular Courses" %}</h3>
</div>
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
{% for course in top_courses %}
<li class="py-5 flex items-center justify-between transition-colors">
<div class="flex items-center gap-5">
<!-- UPGRADED PERFECT CIRCLE BADGE -->
<span
class="flex-shrink-0 flex items-center justify-center rounded-full bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400 font-extrabold text-xl border-2 border-primary-300 dark:border-primary-700"
style="width: 2rem; height: 2rem; min-width: 2rem; min-height: 2rem;">
{{ forloop.counter }}
</span>
<div>
<p class="text-base font-semibold text-gray-900 dark:text-white mb-1">{{ course.title }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400">By
{% if course.professor.fullname %}
{{ course.professor.fullname }}
{% else %}
{{ course.professor.email|default:"Unknown" }}
{% endif %}
</p>
</div>
</div>
<span
class="flex-shrink-0 inline-flex items-center px-4 py-1.5 rounded-full text-sm font-bold bg-primary-100 text-primary-800 dark:bg-primary-900/40 dark:text-primary-300 border border-primary-200 dark:border-primary-700 shadow-sm">
{{ course.participant_count }} {% trans "Participants" %}
</span>
</li>
{% empty %}
<li class="py-4 text-sm text-gray-500">{% trans "No courses found." %}</li>
{% endfor %}
</ul>
{% endcomponent %}
</div>
<!-- Right Column: Success Rate Donut Chart -->
<div>
{% component "unfold/components/card.html" %}
<div class="flex flex-col items-center justify-center text-center py-2">
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-6">{% trans "Transaction Status" %}
</h3>
{% if tx_stats.total > 0 %}
<!-- Multi-Segment CSS SVG Donut -->
<div class="relative w-40 h-40 mb-8">
<svg class="w-full h-full transform -rotate-90" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
<!-- Base Background (Failed - Red) -->
<circle cx="18" cy="18" r="16" fill="none" style="color: #ef4444;" stroke="currentColor"
stroke-width="4" />
<!-- Layer 3: Waiting Approval (Bright Blue) -->
<circle cx="18" cy="18" r="16" fill="none" class="transition-all duration-1000 ease-out"
style="color: #3b82f6;" stroke="currentColor" stroke-width="4" stroke-dasharray="100 100"
stroke-dashoffset="{{ tx_stats.offset_waiting }}" />
<!-- Layer 2: Pending (Yellow) -->
<circle cx="18" cy="18" r="16" fill="none" class="transition-all duration-1000 ease-out"
style="color: #facc15;" stroke="currentColor" stroke-width="4" stroke-dasharray="100 100"
stroke-dashoffset="{{ tx_stats.offset_pending }}" />
<!-- Layer 1: Success (Green) -->
<circle cx="18" cy="18" r="16" fill="none" class="transition-all duration-1000 ease-out"
style="color: #22c55e;" stroke="currentColor" stroke-width="4" stroke-dasharray="100 100"
stroke-dashoffset="{{ tx_stats.offset_success }}" />
</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<span class="text-3xl font-bold text-gray-900 dark:text-white">{{ tx_stats.pct_success }}%</span>
<span class="text-xs text-gray-500">{% trans "Success" %}</span>
</div>
</div>
<!-- Legend -->
<div class="w-full grid grid-cols-2 gap-y-3 gap-x-2 text-sm text-left">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full flex-shrink-0" style="background-color: #22c55e;"></span>
<span class="text-gray-600 dark:text-gray-300">{% trans "Success" %}: <strong
class="text-gray-900 dark:text-white">{{ tx_stats.pct_success }}%</strong></span>
</div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full flex-shrink-0" style="background-color: #facc15;"></span>
<span class="text-gray-600 dark:text-gray-300">{% trans "Pending" %}: <strong
class="text-gray-900 dark:text-white">{{ tx_stats.pct_pending }}%</strong></span>
</div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full flex-shrink-0" style="background-color: #3b82f6;"></span>
<span class="text-gray-600 dark:text-gray-300">{% trans "Waiting" %}: <strong
class="text-gray-900 dark:text-white">{{ tx_stats.pct_waiting }}%</strong></span>
</div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full flex-shrink-0" style="background-color: #ef4444;"></span>
<span class="text-gray-600 dark:text-gray-300">{% trans "Failed" %}: <strong
class="text-gray-900 dark:text-white">{{ tx_stats.pct_failed }}%</strong></span>
</div>
</div>
{% else %}
<div class="py-12 flex flex-col items-center justify-center">
<span class="material-symbols-outlined text-4xl text-gray-300 dark:text-gray-600 mb-2">payments</span>
<p class="text-sm text-gray-500 dark:text-gray-400">{% trans "No transactions recorded yet." %}</p>
</div>
{% endif %}
</div>
{% endcomponent %}
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}

145
utils/admin.py

@ -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')
# Certificates logic respecting permissions
# --- 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()
# --- 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}")

148
utils/unfold_translations.py

@ -1,48 +1,100 @@
# backend/utils/unfold_translations.py
from django.utils.translation import gettext_lazy as _
# Dummy list to trick makemessages into keeping our Unfold overrides safe!
UNFOLD_CUSTOM_STRINGS = [
_("Search"),
_("Search apps and models"),
_("View site"),
_("Log out"),
_("general"),
_("manage all Participants"),
_("questions"),
_("question"),
_("option"),
_("correct answer"),
_("Total score"),
_("Timing score"),
_("Question score"),
_("Total timing"),
_("Ended at"),
_("Started at"),
_("Participant Answer"),
_("Selected option"),
_("Correct Answer"),
_("Seconds take to answer"),
_("Recent Messages Latest"),
_("Room name"),
_("Is Locked"),
_("Initiator"),
_("Recipient"),
_("Manage All Messages"),
_("Sender"),
_("Message Content"),
_("Chat Type"),
_("Sent At"),
_("Is deleted"),
_("Group"),
_("Private"),
_("System Administration"),
_("All Users"),
_("Monday"),
_("Tuesday"),
_("Wednesday"),
_("Thursday"),
_("Friday"),
_("Saturday"),
_("Sunday"),
]
# backend/utils/unfold_translations.py
from django.utils.translation import gettext_lazy as _
# Dummy list to trick makemessages into keeping our Unfold overrides safe!
UNFOLD_CUSTOM_STRINGS = [
_("Search"),
_("Search apps and models"),
_("View site"),
_("Log out"),
_("general"),
_("manage all Participants"),
_("questions"),
_("question"),
_("option"),
_("correct answer"),
_("Total score"),
_("Timing score"),
_("Question score"),
_("Total timing"),
_("Ended at"),
_("Started at"),
_("Participant Answer"),
_("Selected option"),
_("Correct Answer"),
_("Seconds take to answer"),
_("Recent Messages Latest"),
_("Room name"),
_("Is Locked"),
_("Initiator"),
_("Recipient"),
_("Manage All Messages"),
_("Sender"),
_("Message Content"),
_("Chat Type"),
_("Sent At"),
_("Is deleted"),
_("Group"),
_("Private"),
_("System Administration"),
_("All Users"),
_("Monday"),
_("Tuesday"),
_("Wednesday"),
_("Thursday"),
_("Friday"),
_("Saturday"),
_("Sunday"),
_("Active Course"),
_("Active Courses"),
_("30-Day Revenue"),
_("Popular Courses"),
_("Top 5 Popular Courses"),
_("Success"),
_("Pending"),
_("Waiting"),
_("Failed"),
_("Success:"),
_("Pending:"),
_("Waiting:"),
_("Failed:"),
_("Choose file to upload"),
_("Date from"),
_("Date to"),
_("By Date Joined"),
_("Type to search"),
_("Type to search..."),
_("Apply filters"),
_("From"),
_("To"),
_("video link"),
_("Video Link"),
_("Time"),
_("Delete"),
_("Title"),
_("Add Weekly Timing"),
_("Delete All"),
_("Add Course Features"),
_("Is Staff"),
_("Deleted at"),
_("User Type"),
_("Type of the user"),
_("Raw passwords are not stored, so there is no way to see this user’s password, but you can change the password using <a href=\"{}\">this form</a>."),
_("Total Active Users"),
_("Total Guest Users"),
_("Total Students"),
_("Total Professors"),
_("Users:"),
_("users :"),
_("By"),
_("By Date Joined"),
_("( both fields are showing )"),
_("(both fields are showing)"),
_("Imam Javad Admin"),
_("Imam Javad School"),
_("Dovoodi Admin"),
_("Dovodbi Application"),
_("Updated Today"),
_("Requires Action"),
]
Loading…
Cancel
Save