From f8f69868a312870e52ea464e6c3c485fb26cc317 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Mon, 4 May 2026 12:53:37 +0330 Subject: [PATCH] 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 --- .../templates/account/user_list_section.html | 2 +- templates/admin/index.html | 137 +++++++++++++++- utils/admin.py | 145 +++++++++++------ utils/unfold_translations.py | 148 ++++++++++++------ 4 files changed, 328 insertions(+), 104 deletions(-) diff --git a/apps/account/templates/account/user_list_section.html b/apps/account/templates/account/user_list_section.html index 3074961..6c2b419 100644 --- a/apps/account/templates/account/user_list_section.html +++ b/apps/account/templates/account/user_list_section.html @@ -5,7 +5,7 @@
{% component "unfold/components/card.html" %} {% component "unfold/components/text.html" %} - {% trans "Total Actice Users" %} + {% trans "Total Active Users" %} {% endcomponent %} {% component "unfold/components/title.html" with component_class="AllUserComponent" %}{% endcomponent %} diff --git a/templates/admin/index.html b/templates/admin/index.html index 5a05999..e6d2f53 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -20,22 +20,27 @@
-
+ +
{% for item in kpi %} {% component "unfold/components/card.html" %} + {% component "unfold/components/text.html" %} {{ item.title }} {% endcomponent %} - {% component "unfold/components/title.html" %} - {{ item.metric }} - {% endcomponent %} +
+ {% component "unfold/components/title.html" %} + {{ item.metric }} + {% endcomponent %} +
{% if item.footer %} -
+
{{ item.footer|safe }}
{% endif %} + {% endcomponent %} {% empty %}

@@ -43,4 +48,126 @@

{% endfor %}
+ + +{% if top_courses %} +
+ + +
+ {% component "unfold/components/card.html" %} +
+

{% trans "Top 5 Popular Courses" %}

+
+ +
    + {% for course in top_courses %} +
  • +
    + + + + {{ forloop.counter }} + + +
    +

    {{ course.title }}

    +

    By + {% if course.professor.fullname %} + {{ course.professor.fullname }} + {% else %} + {{ course.professor.email|default:"Unknown" }} + {% endif %} +

    +
    +
    + + + {{ course.participant_count }} {% trans "Participants" %} + +
  • + {% empty %} +
  • {% trans "No courses found." %}
  • + {% endfor %} +
+ {% endcomponent %} +
+ + +
+ {% component "unfold/components/card.html" %} +
+

{% trans "Transaction Status" %} +

+ + {% if tx_stats.total > 0 %} + +
+ + + + + + + + + + + + + + + +
+ {{ tx_stats.pct_success }}% + {% trans "Success" %} +
+
+ + +
+
+ + {% trans "Success" %}: {{ tx_stats.pct_success }}% +
+
+ + {% trans "Pending" %}: {{ tx_stats.pct_pending }}% +
+
+ + {% trans "Waiting" %}: {{ tx_stats.pct_waiting }}% +
+
+ + {% trans "Failed" %}: {{ tx_stats.pct_failed }}% +
+
+ + {% else %} +
+ payments +

{% trans "No transactions recorded yet." %}

+
+ {% endif %} +
+ {% endcomponent %} +
+ +
+{% endif %} {% endblock %} \ No newline at end of file diff --git a/utils/admin.py b/utils/admin.py index 7a0f7f9..0f423f1 100644 --- a/utils/admin.py +++ b/utils/admin.py @@ -11,6 +11,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import RedirectView from django.utils.translation import get_language from django.http import JsonResponse +from django.utils.html import format_html from django.views.decorators.http import require_POST # --- AGGRESSIVE UNICODE PATCH FOR UNFOLD 0.64.1 TABS --- @@ -119,10 +120,10 @@ class LoginForm: class FormulaAdminSite(UnfoldAdminSite): """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): super().__init__(*args, **kwargs) @@ -174,9 +175,18 @@ class FormulaAdminSite(UnfoldAdminSite): "500": "107 114 128", "600": "75 85 99", "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": { "50": "234 253 243", @@ -217,10 +227,10 @@ class FormulaAdminSite(UnfoldAdminSite): class DovoodiAdminSite(UnfoldAdminSite): """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): super().__init__(*args, **kwargs) @@ -449,18 +459,18 @@ class HomeView(RedirectView): # --------------------------------------------------------- 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.db.models import Count, Sum + from django.utils import timezone if context is None: context = {} context.update({ "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: @@ -472,34 +482,82 @@ def dashboard_callback(request, context): if is_main_panel(request): try: StudentUser = apps.get_model('account', 'StudentUser') + ProfessorUser = apps.get_model('account', 'ProfessorUser') Course = apps.get_model('course', 'Course') + Blog = apps.get_model('blog', 'Blog') 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') if not request.user.is_staff and not getattr(request.user, 'is_superuser', False): certs_qs = certs_qs.filter(course__professor=request.user) 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"] = [ - { - "title": _("Active Students"), - "metric": f"{StudentUser.objects.filter(is_active=True).count():,}", - "footer": mark_safe('Platform Users'), - }, - { - "title": _("Published Courses"), - "metric": f"{Course.objects.exclude(status='inactive').count():,}", - "footer": mark_safe('Total Offerings'), - }, - { - "title": _("Pending Certificates"), - "metric": f"{pending_certs:,}", - "footer": mark_safe(f'{cert_footer_text}'), - }, + {"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('+ {}', _("Updated Today"))}, + {"title": _("Pending Certificates"), "metric": f"{pending_certs:,}", "footer": format_html('{}', _("Requires Action")) if pending_certs > 0 else ""}, ] except Exception as e: print(f"Dashboard KPI Error (Main Panel): {e}") @@ -509,7 +567,6 @@ def dashboard_callback(request, context): # ------------------------------------------------------------- else: try: - # Safely fetch Dovoodi specific models Video = apps.get_model('video', 'Video') Book = apps.get_model('library', 'Book') Article = apps.get_model('article', 'Article') @@ -520,21 +577,9 @@ def dashboard_callback(request, context): total_reading = Book.objects.count() + Article.objects.count() context["kpi"] = [ - { - "title": _("Hadith Database"), - "metric": f"{Hadis.objects.count():,}", - "footer": mark_safe('Total Records'), - }, - { - "title": _("Books & Articles"), - "metric": f"{total_reading:,}", - "footer": mark_safe('Reading Materials'), - }, - { - "title": _("Multimedia"), - "metric": f"{total_multimedia:,}", - "footer": mark_safe('Videos & Podcasts'), - }, + {"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: print(f"Dashboard KPI Error (Dovoodi Panel): {e}") diff --git a/utils/unfold_translations.py b/utils/unfold_translations.py index 3a699ee..81fdf89 100644 --- a/utils/unfold_translations.py +++ b/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"), -] \ No newline at end of file +# 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 this form."), + _("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"), +] + \ No newline at end of file