From 6a6616f24aa26b446a1f306039b8d53c90ce9c3e Mon Sep 17 00:00:00 2001 From: javad Date: Fri, 24 Jul 2026 22:11:52 +0330 Subject: [PATCH] fix nazar sanji and eghdamha --- .../migrations/0055_action_nextnazardt.py | 18 ++ admin_panel/models.py | 1 + admin_panel/templates/admin_panel/base.html | 5 + .../admin_panel/customerActionInc.html | 126 +++++--- .../admin_panel/customerRezayatinc.html | 34 +- .../templates/admin_panel/report_rezayat.html | 290 ++++++++++++++++++ admin_panel/urls.py | 1 + admin_panel/views.py | 158 ++++++++-- 8 files changed, 542 insertions(+), 91 deletions(-) create mode 100644 admin_panel/migrations/0055_action_nextnazardt.py create mode 100644 admin_panel/templates/admin_panel/report_rezayat.html diff --git a/admin_panel/migrations/0055_action_nextnazardt.py b/admin_panel/migrations/0055_action_nextnazardt.py new file mode 100644 index 0000000..65c6d80 --- /dev/null +++ b/admin_panel/migrations/0055_action_nextnazardt.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2026-07-24 18:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin_panel', '0054_auto_20260715_2250'), + ] + + operations = [ + migrations.AddField( + model_name='action', + name='nextNazarDT', + field=models.DateTimeField(blank=True, default=None, null=True), + ), + ] diff --git a/admin_panel/models.py b/admin_panel/models.py index 0af3a32..b28861c 100644 --- a/admin_panel/models.py +++ b/admin_panel/models.py @@ -461,6 +461,7 @@ class Action(models.Model): result = models.CharField(max_length=500, null=True, blank=True) status = models.CharField(max_length=50, null=True, blank=True) nazar = models.CharField(max_length=50, null=True, blank=True, default=None) + nextNazarDT = models.DateTimeField(null=True, blank=True, default=None) nazarDT = models.DateTimeField(null=True, blank=True, default=None) grade = models.IntegerField(null=True, blank=True, default=None) diff --git a/admin_panel/templates/admin_panel/base.html b/admin_panel/templates/admin_panel/base.html index 2b5f305..0f6f888 100644 --- a/admin_panel/templates/admin_panel/base.html +++ b/admin_panel/templates/admin_panel/base.html @@ -102,6 +102,11 @@ داشبورد مدیریتی + + + گزارش نظر سنجی ها + + diff --git a/admin_panel/templates/admin_panel/customerActionInc.html b/admin_panel/templates/admin_panel/customerActionInc.html index b02f459..412d0d5 100644 --- a/admin_panel/templates/admin_panel/customerActionInc.html +++ b/admin_panel/templates/admin_panel/customerActionInc.html @@ -5,8 +5,62 @@ {% endwith %} {% if customer %} +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+ + حذف فیلتر +
+ +
+
+
ایجاد وظیفه جدید {% endif %} + + {% if not actions and not actionPr %} +
+ هیچ اقدامی با مشخصات انتخاب‌شده یافت نشد! +
+ {% endif %} + {% for action in actions %}
@@ -35,34 +89,28 @@
-
+
{{ action.actionDateTime | toJalali }}
-
-
-
+
{{ action.forUser.first_name }} {{ action.forUser.last_name }}
-
{% endfor %} + {% if actionPr %}
-

کارهای مانده از روزهای قبل

+

کارهای مانده از روزهای قبل

{% for action in actionPr %}
@@ -89,26 +137,23 @@ نتیجه {% if action.result %} {{ action.result }} {% else %}هنوز گزارش یا نتیجه ای ثبت نشده!{% endif %}
-
-
+
{{ action.actionDateTime | toJalali }}
-
-
-
+
{{ action.forUser.first_name }} {{ action.forUser.last_name }}
-
{% endfor %} @@ -127,7 +172,7 @@
- حذف - لغو - + -
+ \ No newline at end of file diff --git a/admin_panel/templates/admin_panel/customerRezayatinc.html b/admin_panel/templates/admin_panel/customerRezayatinc.html index e6148b6..d12b480 100644 --- a/admin_panel/templates/admin_panel/customerRezayatinc.html +++ b/admin_panel/templates/admin_panel/customerRezayatinc.html @@ -61,30 +61,28 @@
- -
- حذف - لغو - + لغو -
@@ -173,14 +167,11 @@ $(".hidephone").addClass("is-hidden"); $("#csDesc").val("") $("#csGrade").val("") - //$("#sabtBtn05").val("ویرایش"); var actionEditUrl = "{% url 'admin_panel:rezayatEdit' 2222 %}"; var action_id = $(this).parent().attr("action_id"); var customer_id = $(this).parent().attr("customer_id"); - //actionEditUrl = actionEditUrl.replace('1111', customer_id); actionEditUrl = actionEditUrl.replace('2222', action_id); - $("#csphone1") $.getJSON('/panel/CustomerJson/'+customer_id, function(data) { CustomerParseData = JSON.parse(data); $("#csName").html(CustomerParseData[0]['fields']['name_family']); @@ -204,12 +195,7 @@ $("#ActionBox").prop("action", actionEditUrl); $.getJSON('/panel/ActionJson/'+action_id, function(data) { ActionParseData = JSON.parse(data); - //console.log(ActionParseData) $("#ActionBox").addClass("is-active"); - //$("#actionType").val(ActionParseData[0]['fields']['actionType']); - //$("#desc").val(ActionParseData[0]['fields']['desc']); - //$("#userID").val(ActionParseData[0]['fields']['forUser']); - //$("#actionResult").val(ActionParseData[0]['fields']['result']); $("#csDesc").val(ActionParseData[0]['fields']['nazar']) $("#csGrade").val(ActionParseData[0]['fields']['grade']) }); @@ -250,7 +236,5 @@ } }, }) - //.setDate(1382976091100); - \ No newline at end of file diff --git a/admin_panel/templates/admin_panel/report_rezayat.html b/admin_panel/templates/admin_panel/report_rezayat.html new file mode 100644 index 0000000..9c303ca --- /dev/null +++ b/admin_panel/templates/admin_panel/report_rezayat.html @@ -0,0 +1,290 @@ +{% extends "./base.html" %} +{% load custom_tags %} +{% load static %} + +{% block bodyPage %} +
+ +
+
+

داشبورد کنترل کیفیت و رضایت مشتریان

+

مانیتورینگ بازخوردها و پیگیری مشتریان ناراضی جهت جلب رضایت مجدد

+
+
+ +
+
+
+
+
+
+

کل نظرسنجی‌های انجام شده

+

{{ total_surveys }} مورد

+
+
+
+
+
+
+
+
+
+
+

میانگین امتیاز سیستم

+

{{ avg_grade }} از ۲۰

+
+
+
+
+
+
+
+
+
+
+

مشتریان ناراضی (نیازمند پیگیری)

+

{{ unhappy_count }} نفر

+
+
+
+
+
+
+ +
+ +
+
+

+ 📊 توزیع سطوح رضایت‌مندی +

+ {% if has_chart_data %} +
+ +
+ {% else %} +
+ هنوز داده‌ای برای رسم نمودار رضایت‌مندی ثبت نشده است. +
+ {% endif %} +
+
+ +
+
+

+ 📞 لیست پیگیری و جلب رضایت (امتیاز ضعیف و بد) +

+ + {% if not low_score_actions %} +
+ خوشبختانه در حال حاضر هیچ مشتری ناراضی برای پیگیری وجود ندارد. +
+ {% endif %} + +
+ + + + + + + + + + + + {% for action in low_score_actions %} + + + + + + + + {% endfor %} + +
مشتریتوضیحات نظرسنجی قبلیامتیازتاریخ ثبتعملیات
+ + {{ action.customer.name_family }} + + + {{ action.nazar|default:"-" }} + + {% if action.grade == 15 %} + متوسط (۱۵) + {% elif action.grade == 10 %} + ضعیف (۱۰) + {% elif action.grade == 5 %} + بد (۵) + {% else %} + {{ action.grade }} + {% endif %} + + {{ action.nazarDT|toJalali }} + + +
+
+
+
+ +
+
+ + + + +{% endblock %} + +{% block jsEnd %} + + + +{% endblock %} \ No newline at end of file diff --git a/admin_panel/urls.py b/admin_panel/urls.py index 8848e3e..29d7a65 100644 --- a/admin_panel/urls.py +++ b/admin_panel/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ path('report/', views.report, name='report'), path('report/inventory-movements/', views.inventory_movements_report, name='inventory_movements_report'), path('report/dashboard/', views.financial_dashboard, name='financial_dashboard'), + path('report/rezayat_dashboard/', views.rezayat_dashboard, name='rezayat_dashboard'), path('hesabdari/', views.hesabdari, name='hesabdari'), path('hesabdari/sarresid_nashode/', views.hesabdari_residnashone, name='hesabdari_residnashone'), path('hesabdari/check/pass/', views.hesabdari_checkPass, name='hesabdari_check_pass'), diff --git a/admin_panel/views.py b/admin_panel/views.py index 95e9834..f57b11a 100644 --- a/admin_panel/views.py +++ b/admin_panel/views.py @@ -13,7 +13,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest # بخشBadRequest جدید from django.db import transaction, connection -from django.db.models import Count, Sum, Q +from django.db.models import Count, Sum, Q, Avg from django.contrib.auth.models import Permission, User from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required, permission_required @@ -1930,11 +1930,39 @@ def customerHesabdariAdd(request ,c_id): @login_required(login_url='/panel/p_login/') -def customerAction(request ,c_id): - customer = Customer.objects.get(id=c_id) - actions = Action.objects.filter(customer=customer).order_by("-createdDT") +def customerAction(request, c_id): + # استفاده از get_object_or_404 برای امنیت بیشتر + customer = get_object_or_404(Customer, id=c_id) + + # دریافت پارامترهای فیلتر از URL (پیش‌فرض: all) + filter_type = request.GET.get('actionType', 'all') + filter_status = request.GET.get('status', 'all') + + # کوئری پایه برای اقدامات این مشتری + actions = Action.objects.filter(customer=customer) + + # اعمال فیلتر بر اساس نوع اقدام (بازدید، نصب و ...) + if filter_type != 'all': + actions = actions.filter(actionType=filter_type) + + # اعمال فیلتر بر اساس وضعیت (انجام شده، در انتظار و ...) + if filter_status != 'all': + actions = actions.filter(status=filter_status) + + # مرتب‌سازی نزولی بر اساس تاریخ ایجاد + actions = actions.order_by("-createdDT") + allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat') - data = {'customer': customer, 'actions': actions, 'allUsers': allUsers} + + data = { + 'customer': customer, + 'actions': actions, + 'allUsers': allUsers, + # ارسال مقادیر فیلتر به قالب برای حفظ وضعیت انتخاب‌شده در دراپ‌داون‌ها + 'filter_type': filter_type, + 'filter_status': filter_status + } + return render(request, 'admin_panel/customerAction.html', data) @login_required(login_url='/panel/p_login/') @@ -2089,16 +2117,40 @@ def ActionListUserToday(request, u_id): data = {'actions': actions, 'allUsers': allUsers, 'u_id': u_id, "unixADT": unixADT, "status": status, 'actionPr': actionPr} return render(request, 'admin_panel/ActionList.html', data) +import datetime +from django.db.models import Q +from django.shortcuts import render +from django.contrib.auth.decorators import login_required +from .models import Action + @login_required(login_url='/panel/p_login/') def rezayat(request): - dateTimeAction = request.GET.get('dateTime', None) - statusRezayat = request.GET.get('status', None) - status='finish' - if statusRezayat=='finish': - actions = Action.objects.exclude(actionType='vosol').exclude(Q(nazar='') | Q(nazar=None)).filter(status='finish').order_by("-id")[:50] + statusRezayat = request.GET.get('status', 'waite') + now = datetime.datetime.now() + + # محاسبه بازه‌های زمانی ۳۰ روز و ۶۰ روز قبل + dt_30_days_ago = now - datetime.timedelta(days=30) + dt_60_days_ago = now - datetime.timedelta(days=60) + + if statusRezayat == 'finish': + # نمایش نظرسنجی‌های تعیین‌تکلیف شده (ثبت شده یا کنسل شده) + actions = Action.objects.exclude(actionType='vosol').filter( + status='finish', + nazarDT__isnull=False + ).order_by("-nazarDT")[:500] else: - statusRezayat='waite' - actions = Action.objects.filter(Q(nazar='') | Q(nazar=None) & Q(status='finish')).exclude(actionType='vosol').order_by("id")[:50] + statusRezayat = 'waite' + # نمایش نظرسنجی‌های در انتظار با اعمال شرط عدم انقضای زمان + actions = Action.objects.exclude(actionType='vosol').filter( + status='finish', + nazarDT__isnull=True, # نظرسنجی هنوز نهایی نشده + createdDT__gte=dt_60_days_ago # حداکثر ۶۰ روز از ایجاد گذشته باشد + ).filter( + Q(finishDateTime__isnull=True) | Q(finishDateTime__gte=dt_30_days_ago) # حداکثر ۳۰ روز از اتمام گذشته باشد + ).filter( + Q(nextNazarDT__isnull=True) | Q(nextNazarDT__lte=now) # موعد پیگیری رسیده یا تعویقی نداشته باشد + ).order_by("-id")[:500] + data = {'actions': actions, "status": statusRezayat} return render(request, 'admin_panel/customerRezayat.html', data) @@ -2107,19 +2159,87 @@ def rezayat(request): def rezayatEdit(request, ac_id): csGrade = request.POST.get('csGrade', None) csDesc = request.POST.get('csDesc', None) + if csDesc and csGrade: - action = Action.objects.get(id=ac_id) - action.nazar = csDesc - action.grade = csGrade - action.nazarDT = datetime.datetime.now() + action = get_object_or_404(Action, id=ac_id) + + if csGrade == '-1': + # عدم موفقیت: تنظیم nextNazarDT برای ۳ روز بعد (nazarDT پر نمی‌شود تا نهایی نشود) + action.nazar = csDesc + action.grade = int(csGrade) + action.nextNazarDT = datetime.datetime.now() + datetime.timedelta(days=3) + + elif csGrade == '-2': + # کنسل کردن: پر کردن nazarDT برای خروج کامل از لیست کارهای در انتظار + action.nazar = csDesc + action.grade = int(csGrade) + action.nazarDT = datetime.datetime.now() + action.nextNazarDT = None + + else: + # ثبت امتیاز عادی + action.nazar = csDesc + action.grade = int(csGrade) + action.nazarDT = datetime.datetime.now() + action.nextNazarDT = None + action.save() return redirect('/panel/rezayat/') else: - return HttpResponse('لطفا همه فیلد ها را پر کنید') - #data = {'actions': actions, "status": status} - #return render(request, 'admin_panel/customerRezayat.html', data) + return HttpResponse('لطفا همه فیلد ها را پر کنید') +@login_required(login_url='/panel/p_login/') +@permission_required('admin_panel.ViewReportMali', raise_exception=True) +def rezayat_dashboard(request): + # دریافت تمامی نظرسنجی‌های ثبت شده معتبر (که تاریخ نظرسنجی دارند) + valid_surveys = Action.objects.filter( + nazarDT__isnull=False, + grade__in=[5, 10, 15, 20] + ) + + # ۱. محاسبات شاخص‌های کلیدی (KPIs) + total_surveys = valid_surveys.count() + avg_grade = valid_surveys.aggregate(Avg('grade'))['grade__avg'] or 0 + + # ۲. استخراج داده‌ها برای نمودار توزیع رضایت + grade_distribution = valid_surveys.values('grade').annotate(count=Count('id')) + chart_dict = {20: 0, 15: 0, 10: 0, 5: 0} + for item in grade_distribution: + g = item['grade'] + if g in chart_dict: + chart_dict[g] = item['count'] + + chart_labels = ['عالی (۲۰)', 'متوسط (۱۵)', 'ضعیف (۱۰)', 'بد (۵)'] + chart_data_list = [chart_dict[20], chart_dict[15], chart_dict[10], chart_dict[5]] + + # ۳. تبدیل امن آرایه‌ها به JSON برای Chart.js + chart_labels_json = json.dumps(chart_labels, ensure_ascii=False) + chart_data_json = json.dumps(chart_data_list) + has_chart_data = sum(chart_data_list) > 0 + + # ۴. واکشی لیست مشتریان نیازمند پیگیری (شامل متوسط ۱۵، ضعیف ۱۰ و بد ۵) + low_score_actions = Action.objects.filter( + nazarDT__isnull=False, + grade__in=[5, 10, 15] # <-- امتیاز ۱۵ (متوسط) نیز اضافه شد + ).order_by('-nazarDT') + + allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat') + + context = { + 'total_surveys': total_surveys, + 'avg_grade': round(avg_grade, 1), + 'chart_labels_json': chart_labels_json, + 'chart_data_json': chart_data_json, + 'has_chart_data': has_chart_data, + 'low_score_actions': low_score_actions, + # تعداد کل موارد نیازمند پیگیری (متوسط + ضعیف + بد) + 'unhappy_count': chart_dict[15] + chart_dict[10] + chart_dict[5], + 'allUsers': allUsers, + } + + return render(request, 'admin_panel/report_rezayat.html', context) + @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.ViewStore', raise_exception=True) def passCheck(request, articl_id):