improve bazdid 4
This commit is contained in:
1 parent
f86d060e4d
commit
8fd881e0ea
10 files changed
+1752
-320
No files matched your search
+381
-76
@@ -19,19 +19,47 @@ from ..sms.phones import (normalize_primary_values,
|
||||
from ..sms.triggers import notify_payment_thanks, notify_welcome
|
||||
from ..sms.rules import customer_toggle_map
|
||||
from ..visit_policy import (SOURCE_COORDINATION, SOURCE_MANUAL, SOURCE_SCHEDULED,
|
||||
appointment_sources_map, last_visit_map, visit_due_at,
|
||||
visit_due_date, visit_period_months)
|
||||
appointment_sources_map, as_aware,
|
||||
followups_on_date, last_visit_map, overdue_followups,
|
||||
visit_due_at, visit_due_date, visit_period_months)
|
||||
|
||||
# نماهای صفحهٔ «انتظار» (فیلتر ?view=). پیشفرض = فقط «بدون قرار» تا رفتار
|
||||
# قبلی صفحه برای کاربران حفظ شود.
|
||||
ENTEZAR_VIEWS = ('uncoordinated', 'manual', 'scheduled', 'coordination',
|
||||
# نماهای صفحهٔ «انتظار» (فیلتر ?view=).
|
||||
# پیشفرض = «نیازمند پیگیری امروز»: فهرست کاریِ روزانهٔ کارشناس (موعد تماسهای
|
||||
# امروز). سایر نماها با کارتها/دراپداون «نوع قرار» در دسترساند.
|
||||
# ``today`` یک نمای **مستقل** است: پایهٔ آن «عقبافتاده بودن» نیست — مشتری
|
||||
# سالمی هم که با او قرار تماس گذاشته شده باید در این فهرست دیده شود.
|
||||
ENTEZAR_VIEWS = ('today', 'uncoordinated', 'manual', 'scheduled', 'coordination',
|
||||
'coordinated', 'all')
|
||||
ENTEZAR_DEFAULT_VIEW = 'uncoordinated'
|
||||
# حالتهای مرتبسازی (?sort=):
|
||||
# oldest ⇒ قدیمیترین بازدید اول (پیشفرض/اولویت کارشناس)
|
||||
# newest ⇒ جدیدترین بازدید اول
|
||||
# overdue ⇒ بیشترین تأخیر اول
|
||||
ENTEZAR_SORTS = ('oldest', 'newest', 'overdue')
|
||||
ENTEZAR_DEFAULT_VIEW = 'today'
|
||||
# نمای «نیازمند پیگیری» — برای بررسی «آیا این نما فعال است؟» در قالب/ویو.
|
||||
ENTEZAR_FOLLOWUP_VIEW = 'today'
|
||||
|
||||
# تعداد ردیف در هر صفحه. فهرست کاری روزانه با یک نگاه مرور میشود، پس صفحهبندی
|
||||
# دیرتر فعال شود.
|
||||
ENTEZAR_PAGE_SIZE = 100
|
||||
|
||||
# فیلتر «میزان تأخیر» (?delay=). مرزها با «مدت تأخیر» (روز) مقایسه میشوند.
|
||||
ENTEZAR_DELAYS = ('lt30', '30to90', '90to180', 'gt180')
|
||||
|
||||
# ستونهای قابل مرتبسازی: نام فیلد ⇒ جهت پیشفرضِ اولین کلیک.
|
||||
# ``oldest``/``newest``/``overdue`` مقادیر قدیمیاند و برای سازگاری با
|
||||
# بوکمارکهای قبلی نگه داشته شدهاند (به ``last_visit``/``overdue`` نگاشت میشوند).
|
||||
ENTEZAR_SORT_COLUMNS = {
|
||||
'name': 'asc',
|
||||
'status': 'asc',
|
||||
'manual': 'desc',
|
||||
'scheduled': 'desc',
|
||||
'coordination': 'desc',
|
||||
'last_visit': 'asc',
|
||||
'overdue': 'desc',
|
||||
'phone': 'asc',
|
||||
'address': 'asc',
|
||||
}
|
||||
_ENTEZAR_LEGACY_SORTS = {
|
||||
'oldest': ('last_visit', 'asc'),
|
||||
'newest': ('last_visit', 'desc'),
|
||||
'overdue': ('overdue', 'desc'),
|
||||
}
|
||||
from ._helpers import logger, _parse_jalali_date, _parse_phone_rows
|
||||
|
||||
|
||||
@@ -359,9 +387,26 @@ def entezar(request):
|
||||
view = (request.GET.get('view') or ENTEZAR_DEFAULT_VIEW).strip().lower()
|
||||
if view not in ENTEZAR_VIEWS:
|
||||
view = ENTEZAR_DEFAULT_VIEW # مقدار نامعتبر ⇒ پیشفرض، بدون خطا
|
||||
sort = (request.GET.get('sort') or 'oldest').strip().lower()
|
||||
if sort not in ENTEZAR_SORTS:
|
||||
sort = 'oldest'
|
||||
delay = (request.GET.get('delay') or '').strip().lower()
|
||||
if delay not in ENTEZAR_DELAYS:
|
||||
delay = '' # '' یعنی بدون فیلتر تأخیر
|
||||
|
||||
# مرتبسازی: ``?sort=<ستون>&dir=asc|desc``؛ مقادیر قدیمی هم پذیرفته میشوند.
|
||||
# نکته: اگر ``dir`` صریح آمده باشد، مقدم است — وگرنه کلیک روی سرستون
|
||||
# «تأخیر» با جهت صعودی، بهخاطر نگاشت قدیمی، نادیده گرفته میشد.
|
||||
raw_sort = (request.GET.get('sort') or '').strip().lower()
|
||||
raw_dir = (request.GET.get('dir') or '').strip().lower()
|
||||
if raw_sort in ENTEZAR_SORT_COLUMNS:
|
||||
sort_field = raw_sort
|
||||
if raw_dir in ('asc', 'desc'):
|
||||
sort_dir = raw_dir
|
||||
else:
|
||||
sort_dir = 'desc' if raw_sort in _ENTEZAR_LEGACY_SORTS else \
|
||||
ENTEZAR_SORT_COLUMNS[raw_sort]
|
||||
elif raw_sort in _ENTEZAR_LEGACY_SORTS:
|
||||
sort_field, sort_dir = _ENTEZAR_LEGACY_SORTS[raw_sort]
|
||||
else:
|
||||
sort_field, sort_dir = _ENTEZAR_LEGACY_SORTS['oldest']
|
||||
searchStatus = True
|
||||
full_path = request.get_full_path()
|
||||
|
||||
@@ -370,32 +415,47 @@ def entezar(request):
|
||||
# دوره فقط یک بار خوانده میشود (جلوگیری از N+1 در حلقهٔ محاسبهٔ تأخیر).
|
||||
period_months = visit_period_months()
|
||||
# مرز تاریخ به وقت محلی (نه UTC) تا صفحه و اسکنر پیامک یک جواب بدهند.
|
||||
due_cut_date = due_cut.astimezone(timezone.get_current_timezone()).date()
|
||||
current_tz = timezone.get_current_timezone()
|
||||
today_date = now.astimezone(current_tz).date()
|
||||
due_cut_date = due_cut.astimezone(current_tz).date()
|
||||
|
||||
# تاریخ نمای «نیازمند پیگیری»: از تقویم شمسی میآید (``?fdate=1405/08/29``).
|
||||
# خالی/نامعتبر ⇒ امروز. مقدار گذشته و آینده هر دو مجازند.
|
||||
followup_date_raw = (request.GET.get('fdate') or '').strip()
|
||||
followup_date = _parse_jalali_date(followup_date_raw) or today_date
|
||||
followup_date_jalali = JalaliDate(followup_date).strftime('%Y/%m/%d')
|
||||
is_followup_view = view == ENTEZAR_FOLLOWUP_VIEW
|
||||
|
||||
visits = last_visit_map(now=now)
|
||||
sources = appointment_sources_map(now=now)
|
||||
|
||||
# جستوجو: نام، آدرس، تلفن (همه شمارهها) و کد شناسایی.
|
||||
def _apply_search(queryset):
|
||||
"""فیلتر جستوجو؛ فیلد نامعتبر ⇒ بدون فیلتر (سازگاری با رفتار قبلی)."""
|
||||
if not searchVal:
|
||||
return queryset
|
||||
if searchOn == 'address':
|
||||
return queryset.filter(address__contains=searchVal)
|
||||
if searchOn == 'name':
|
||||
return queryset.filter(name_family__contains=searchVal)
|
||||
if searchOn == 'phone':
|
||||
return queryset.filter(
|
||||
Q(phone1__contains=searchVal) | Q(phone2__contains=searchVal)
|
||||
| Q(phone3__contains=searchVal)
|
||||
)
|
||||
if searchOn == 'code':
|
||||
return queryset.filter(idCode__contains=searchVal)
|
||||
return queryset
|
||||
|
||||
searchStatus = bool(searchVal) and searchOn in (
|
||||
'address', 'name', 'phone', 'code')
|
||||
|
||||
# پایهٔ فهرست انتظار: فقط مشتریانی که دورهشان گذشته (کارتهای آمار).
|
||||
customers_objj = Customer.objects.filter(
|
||||
laghv_bazdid=False, name_family__isnull=False,
|
||||
installDate__lte=due_cut_date,
|
||||
)
|
||||
# جستوجو: نام، آدرس، تلفن (همه شمارهها) و کد شناسایی.
|
||||
if searchVal:
|
||||
if searchOn == 'address':
|
||||
customers_objj = customers_objj.filter(address__contains=searchVal)
|
||||
elif searchOn == 'name':
|
||||
customers_objj = customers_objj.filter(name_family__contains=searchVal)
|
||||
elif searchOn == 'phone':
|
||||
customers_objj = customers_objj.filter(
|
||||
Q(phone1__contains=searchVal) | Q(phone2__contains=searchVal)
|
||||
| Q(phone3__contains=searchVal)
|
||||
)
|
||||
elif searchOn == 'code':
|
||||
customers_objj = customers_objj.filter(idCode__contains=searchVal)
|
||||
else:
|
||||
searchStatus = False
|
||||
else:
|
||||
searchStatus = False
|
||||
customers_objj = _apply_search(customers_objj)
|
||||
|
||||
# مشتریان عقبافتاده: بدون بازدید معتبر، یا بازدید معتبرشان پیش از مقطع دوره.
|
||||
stale_ids = [
|
||||
@@ -431,12 +491,79 @@ def entezar(request):
|
||||
'coordinated': coordinated_ids,
|
||||
'all': stale_ids,
|
||||
}
|
||||
|
||||
# ---------- نمای «نیازمند پیگیری» (یک تاریخ مشخص) ----------
|
||||
# این نما **مستقل** از «عقبافتاده» است: مشتری سالمی که با او قرار تماس
|
||||
# گذاشته شده هم باید دیده شود. قاعدهٔ حذف «بازدید» در خود
|
||||
# ``followups_on_date`` اعمال میشود (مشتریِ دارای قرار بازدید نمیآید).
|
||||
today_followups = followups_on_date(today_date, now=now)
|
||||
|
||||
# پیگیریهای عقبافتاده: موعدشان گذشته و هنوز تعیین تکلیف نشدهاند.
|
||||
# فقط در نمای پیگیری محاسبه میشود (کوئری اضافه برای بقیهٔ نماها لازم نیست).
|
||||
overdue_map = overdue_followups(now=now, target_date=today_date) \
|
||||
if is_followup_view else {}
|
||||
overdue_oldest = min(overdue_map.values()) if overdue_map else None
|
||||
|
||||
# فیلتر «فقط عقبافتادهها» (?missed=1) — همان نما با منبعِ دیگر.
|
||||
show_missed = is_followup_view and \
|
||||
(request.GET.get('missed') or '').strip().lower() in ('1', 'true', 'on')
|
||||
|
||||
if is_followup_view:
|
||||
if show_missed:
|
||||
# تاریخِ موعدِ ازدسترفته بهصورت datetime تا با بقیهٔ مسیرها یکسان باشد.
|
||||
followups = {
|
||||
cid: {SOURCE_COORDINATION: as_aware(day)}
|
||||
for cid, day in overdue_map.items()
|
||||
}
|
||||
elif followup_date == today_date:
|
||||
followups = today_followups
|
||||
else:
|
||||
followups = followups_on_date(followup_date, now=now)
|
||||
else:
|
||||
followups = {}
|
||||
|
||||
def _followup_ids(mapping):
|
||||
"""شناسههای واجد شرایط (فعال/لغونشده/دارای نام + جستوجو) با ترتیب زمانی."""
|
||||
if not mapping:
|
||||
return []
|
||||
eligible = _apply_search(
|
||||
Customer.objects.filter(
|
||||
id__in=list(mapping),
|
||||
is_active=True,
|
||||
laghv_bazdid=False,
|
||||
name_family__isnull=False,
|
||||
)
|
||||
)
|
||||
allowed = set(eligible.values_list('id', flat=True))
|
||||
return sorted(
|
||||
(cid for cid in mapping if cid in allowed),
|
||||
key=lambda cid: (min(mapping[cid].values()), cid),
|
||||
)
|
||||
|
||||
followup_ids = _followup_ids(followups) if is_followup_view else []
|
||||
# شمارندهها از همان مسیرِ فهرست میآیند تا «عدد هشدار = تعداد ردیفها» بماند
|
||||
# (مشتری غیرفعال/لغوشده/بینام یا فیلترشده شمرده نمیشود).
|
||||
today_followup_count = len(_followup_ids(today_followups))
|
||||
overdue_followup_count = len(_followup_ids(
|
||||
{cid: {SOURCE_COORDINATION: as_aware(day)} for cid, day in overdue_map.items()}
|
||||
))
|
||||
selected_map[ENTEZAR_FOLLOWUP_VIEW] = followup_ids
|
||||
|
||||
selected = selected_map[view]
|
||||
|
||||
# برای نمای پیگیری، فهرست مشتریان از همان شناسههای تأییدشده میآید
|
||||
# (ممکن است در «عقبافتاده» نباشند).
|
||||
if is_followup_view:
|
||||
customers_objj = _apply_search(
|
||||
Customer.objects.filter(id__in=followup_ids)
|
||||
)
|
||||
|
||||
# «مدت تأخیر» = از چه زمانی بازدید عقب افتاده است (به روز).
|
||||
# مبنای محاسبه «موعد بازدید» است، نه خود بازدید: موعد = بازدید + دوره.
|
||||
# اینطور مشتریِ تازهمعوق هم در بازهٔ کوتاه قرار میگیرد و عدد معنا پیدا میکند.
|
||||
today = now.astimezone(timezone.get_current_timezone()).date()
|
||||
# محاسبه روی کلِ نمای جاری انجام میشود تا شمارندههای بازهها با فیلتر
|
||||
# «میزان تأخیر» همخوان باشند (اول نمای جاری، بعد فیلتر تأخیر).
|
||||
today = today_date
|
||||
overdue_days = {}
|
||||
for customer_id in selected:
|
||||
anchor = visits.get(customer_id)
|
||||
@@ -448,42 +575,7 @@ def entezar(request):
|
||||
base = due_cut_date
|
||||
overdue_days[customer_id] = max((today - base).days, 0)
|
||||
|
||||
# مرتبسازی:
|
||||
# oldest ⇒ قدیمیترین بازدید اول (اولویت کارشناس)
|
||||
# newest ⇒ جدیدترین بازدید اول
|
||||
# overdue ⇒ بیشترین تأخیر اول (چه کسی بیشتر از همه عقب افتاده)
|
||||
if sort == 'overdue':
|
||||
ordered_ids = sorted(selected, key=lambda cid: (-overdue_days.get(cid, 0), cid))
|
||||
customers_objj = customers_objj.filter(id__in=ordered_ids)
|
||||
if ordered_ids:
|
||||
# ترتیب در لایهٔ دیتابیس با CASE/WHEN (بدون بار حافظهای).
|
||||
ordering = Case(
|
||||
*[When(id=cid, then=Value(index))
|
||||
for index, cid in enumerate(ordered_ids)],
|
||||
output_field=models.IntegerField(),
|
||||
)
|
||||
customers_objj = customers_objj.order_by(ordering)
|
||||
else:
|
||||
customers_objj = customers_objj.order_by('id')
|
||||
else:
|
||||
# مرتبسازی بر مبنای **تاریخ مؤثر بازدید** (از Action)، نه ستون ممکنالقدمت.
|
||||
ordered_ids = sorted(
|
||||
selected,
|
||||
key=lambda cid: (
|
||||
(visits.get(cid) or due_cut),
|
||||
cid,
|
||||
),
|
||||
reverse=(sort == 'newest'),
|
||||
)
|
||||
customers_objj = customers_objj.filter(id__in=ordered_ids)
|
||||
ordering = Case(
|
||||
*[When(id=cid, then=Value(index))
|
||||
for index, cid in enumerate(ordered_ids)],
|
||||
output_field=models.IntegerField(),
|
||||
)
|
||||
customers_objj = customers_objj.order_by(ordering)
|
||||
|
||||
# آمار «تأخیر» (روی کل گروه انتخابی، نه فقط صفحهٔ جاری).
|
||||
# آمار «تأخیر» روی نمای جاری (قبل از اعمال فیلتر تأخیر).
|
||||
buckets = {'lt30': 0, '30to90': 0, '90to180': 0, 'gt180': 0}
|
||||
for days in overdue_days.values():
|
||||
if days < 30:
|
||||
@@ -495,17 +587,110 @@ def entezar(request):
|
||||
else:
|
||||
buckets['gt180'] += 1
|
||||
|
||||
# اعمال فیلتر «میزان تأخیر».
|
||||
delay_ranges = {
|
||||
'lt30': (0, 30),
|
||||
'30to90': (30, 90),
|
||||
'90to180': (90, 180),
|
||||
'gt180': (180, None),
|
||||
}
|
||||
# فیلتر «میزان تأخیر» فقط برای نمای «عقبافتاده» معنا دارد؛ در نمای
|
||||
# «پیگیری امروز» مشتری ممکن است اصلاً تأخیری نداشته باشد.
|
||||
if delay and not is_followup_view:
|
||||
low, high = delay_ranges[delay]
|
||||
selected = [
|
||||
cid for cid in selected
|
||||
if overdue_days.get(cid, 0) >= low
|
||||
and (high is None or overdue_days.get(cid, 0) < high)
|
||||
]
|
||||
|
||||
# ---------- مرتبسازی (روی همهٔ ستونها) ----------
|
||||
# یک کوئری سبک برای فیلدهای Customer که برای مرتبسازی لازم است.
|
||||
info = {
|
||||
row['id']: row
|
||||
for row in Customer.objects.filter(id__in=selected).values(
|
||||
'id', 'name_family', 'phone1', 'address')
|
||||
} if selected else {}
|
||||
|
||||
def _source_date(cid, key):
|
||||
return (sources.get(cid) or {}).get(key)
|
||||
|
||||
def _sort_value(cid):
|
||||
"""مقدار مرتبسازی برای هر ستون (تاریخها aware یا None)."""
|
||||
if sort_field == 'name':
|
||||
return (info.get(cid, {}).get('name_family') or '')
|
||||
if sort_field == 'phone':
|
||||
return (info.get(cid, {}).get('phone1') or '')
|
||||
if sort_field == 'address':
|
||||
return (info.get(cid, {}).get('address') or '')
|
||||
if sort_field == 'status':
|
||||
# ترتیب منطقی: بدون قرار ← قرار دستی ← بازدید ← هماهنگی ← چندقراره
|
||||
entry = sources.get(cid) or {}
|
||||
if not entry:
|
||||
return 0
|
||||
if len(entry) > 1:
|
||||
return 4
|
||||
if SOURCE_MANUAL in entry:
|
||||
return 1
|
||||
if SOURCE_SCHEDULED in entry:
|
||||
return 2
|
||||
return 3
|
||||
if sort_field == 'manual':
|
||||
return _source_date(cid, SOURCE_MANUAL)
|
||||
if sort_field == 'scheduled':
|
||||
return _source_date(cid, SOURCE_SCHEDULED)
|
||||
if sort_field == 'coordination':
|
||||
return _source_date(cid, SOURCE_COORDINATION)
|
||||
if sort_field == 'overdue':
|
||||
return overdue_days.get(cid, 0)
|
||||
# last_visit: مشتریِ بدون بازدید، قدیمیترین حساب میشود.
|
||||
return visits.get(cid) or due_cut
|
||||
|
||||
if selected:
|
||||
if is_followup_view and not raw_sort:
|
||||
# در نمای پیگیری، ترتیب طبیعی «زودترین موعد پیگیری» است
|
||||
# (ترتیب از ``_followup_ids`` آمده) — نه قدیمیترین بازدید.
|
||||
ordered_ids = list(selected)
|
||||
else:
|
||||
values = {cid: _sort_value(cid) for cid in selected}
|
||||
with_value = [cid for cid in selected if values[cid] is not None]
|
||||
without_value = [cid for cid in selected if values[cid] is None]
|
||||
with_value.sort(key=lambda cid: (values[cid], cid),
|
||||
reverse=(sort_dir == 'desc'))
|
||||
# مقادیر خالی همیشه آخر میآیند (مستقل از جهت مرتبسازی) تا فهرست
|
||||
# با ستونهای «قرار دستی/بازدید/هماهنگی» بیمعنا نشود.
|
||||
ordered_ids = with_value + without_value
|
||||
else:
|
||||
ordered_ids = []
|
||||
|
||||
customers_objj = customers_objj.filter(id__in=ordered_ids)
|
||||
if ordered_ids:
|
||||
ordering = Case(
|
||||
*[When(id=cid, then=Value(index))
|
||||
for index, cid in enumerate(ordered_ids)],
|
||||
output_field=models.IntegerField(),
|
||||
)
|
||||
customers_objj = customers_objj.order_by(ordering)
|
||||
else:
|
||||
customers_objj = customers_objj.order_by('id')
|
||||
|
||||
counts = {
|
||||
'total': len(stale_ids),
|
||||
'uncoordinated': len(selected_map['uncoordinated']),
|
||||
# عددِ هر کارت دقیقاً همان چیزی است که با کلیک روی آن دیده میشود؛
|
||||
# چون یک مشتری میتواند چند منبع داشته باشد، جمع کارتها ممکن است
|
||||
# از «با قرار» بیشتر شود. در راهنمای بالای صفحه توضیح داده میشود.
|
||||
# عددِ هر کارت دقیقاً همان چیزی است که با کلیک روی آن دیده میشود.
|
||||
# یک مشتری میتواند چند منبع داشته باشد، پس جمع سه منبع میتواند از
|
||||
# «تعیین تکلیف شده» بیشتر شود؛ جزئیات در دراپداون «نوع قرار» است.
|
||||
'manual': len(selected_map['manual']),
|
||||
'scheduled': len(selected_map['scheduled']),
|
||||
'coordination': len(selected_map['coordination']),
|
||||
'coordinated': len(selected_map['coordinated']),
|
||||
# کارت «نیازمند پیگیری امروز» — همیشه عدد امروز را نشان میدهد،
|
||||
# مستقل از تاریخی که کاربر در تقویم انتخاب کرده است.
|
||||
'followup_today': today_followup_count,
|
||||
# پیگیریهای موعدگذشته که هنوز تعیین تکلیف نشدهاند (هشدار قرمز).
|
||||
'followup_overdue': overdue_followup_count,
|
||||
'selected': len(selected),
|
||||
'view_total': len(selected_map[view]),
|
||||
'buckets': buckets,
|
||||
# برای متن راهنمای بالای صفحه («بیش از N ماه از بازدید گذشته»).
|
||||
'period_months': period_months,
|
||||
@@ -513,20 +698,118 @@ def entezar(request):
|
||||
|
||||
try:
|
||||
entezar_bazdid = CacheTable.objects.get(key='9mahbazdid')
|
||||
# کارت داشبورد = «کارِ فوری» = بدون قرار.
|
||||
# کارت داشبورد = «کارِ فوری» = تعیین تکلیف نشده.
|
||||
entezar_bazdid.value = counts['uncoordinated']
|
||||
entezar_bazdid.save()
|
||||
except CacheTable.DoesNotExist:
|
||||
pass
|
||||
|
||||
paginator = Paginator(customers_objj, 20)
|
||||
paginator = Paginator(customers_objj, ENTEZAR_PAGE_SIZE)
|
||||
page_number = request.GET.get('page', None)
|
||||
customerHa = paginator.get_page(page_number)
|
||||
|
||||
# لینک صفحهبندی با حفظ همهٔ فیلترهای فعلی.
|
||||
def _page_link(number):
|
||||
params = request.GET.copy()
|
||||
params['page'] = number
|
||||
return '?' + params.urlencode()
|
||||
|
||||
page_prev = _page_link(customerHa.previous_page_number()) if customerHa.has_previous() else ''
|
||||
page_next = _page_link(customerHa.next_page_number()) if customerHa.has_next() else ''
|
||||
|
||||
# ---------- لینکهای حفظکنندهٔ فیلترها ----------
|
||||
def _qs(**overrides):
|
||||
"""پارامترهای فعلی + تغییرهای دادهشده (بدون page). None ⇒ حذف."""
|
||||
params = request.GET.copy()
|
||||
params.pop('page', None)
|
||||
for key, value in overrides.items():
|
||||
if value is None:
|
||||
params.pop(key, None)
|
||||
else:
|
||||
params[key] = value
|
||||
query = params.urlencode()
|
||||
return '?' + query if query else '?'
|
||||
|
||||
view_links = {name: _qs(view=name) for name in ENTEZAR_VIEWS}
|
||||
# کارت «نیازمند پیگیری امروز» همیشه به امروز میرود (تاریخ انتخابی پاک میشود).
|
||||
view_links[ENTEZAR_FOLLOWUP_VIEW] = _qs(view=ENTEZAR_FOLLOWUP_VIEW, fdate=None)
|
||||
|
||||
# ---------- ناوبری تقویم در نمای پیگیری ----------
|
||||
def _followup_link(target_date):
|
||||
return _qs(view=ENTEZAR_FOLLOWUP_VIEW,
|
||||
fdate=JalaliDate(target_date).strftime('%Y/%m/%d'))
|
||||
|
||||
followup_nav = {
|
||||
'prev': _followup_link(followup_date - datetime.timedelta(days=1)),
|
||||
'next': _followup_link(followup_date + datetime.timedelta(days=1)),
|
||||
'today': _followup_link(today_date),
|
||||
'is_today': followup_date == today_date,
|
||||
}
|
||||
# لینک هشدار «عقبافتادهها» و لینک بازگشت از آن حالت.
|
||||
followup_overdue_link = _qs(view=ENTEZAR_FOLLOWUP_VIEW, fdate=None, missed=1)
|
||||
followup_ontime_link = _qs(view=ENTEZAR_FOLLOWUP_VIEW, fdate=None, missed=None)
|
||||
|
||||
delay_links = {'ez_all': _qs(delay=None)}
|
||||
delay_links.update({name: _qs(delay=name) for name in ENTEZAR_DELAYS})
|
||||
sort_links = {}
|
||||
for column, default_dir in ENTEZAR_SORT_COLUMNS.items():
|
||||
# کلیک دوباره روی همان ستون، جهت را برمیگرداند.
|
||||
next_dir = ('desc' if sort_dir == 'asc' else 'asc') \
|
||||
if sort_field == column else default_dir
|
||||
sort_links[column] = _qs(sort=column, dir=next_dir)
|
||||
|
||||
# سرستونهای مرتبشدنی (به همان ترتیب ستونهای جدول).
|
||||
# در نمای «نیازمند پیگیری امروز» ستون «تأخیر» نمایش داده **نمیشود**:
|
||||
# این فهرست دربارهٔ «موعد امروز» است، نه عقبافتادگی بازدید.
|
||||
columns = [
|
||||
('name', 'نام و نام خانوادگی', 21),
|
||||
('status', 'وضعیت قرار', 12),
|
||||
('manual', 'قرار دستی', 10),
|
||||
('scheduled', 'بازدید برنامهریزیشده', 10),
|
||||
('coordination', 'هماهنگی', 10),
|
||||
('last_visit', 'آخرین بازدید', 9),
|
||||
('overdue', 'تأخیر', 8),
|
||||
('phone', 'تلفن', 9),
|
||||
('address', 'آدرس', 11),
|
||||
]
|
||||
if is_followup_view:
|
||||
hidden_width = next(width for key, _, width in columns if key == 'overdue')
|
||||
columns = [col for col in columns if col[0] != 'overdue']
|
||||
# عرض ستون حذفشده به نام اضافه میشود تا مجموع همچنان ۱۰۰٪ بماند.
|
||||
columns[0] = (columns[0][0], columns[0][1], columns[0][2] + hidden_width)
|
||||
|
||||
table_columns = [{'key': key, 'label': label, 'width': width}
|
||||
for key, label, width in columns]
|
||||
hidden_columns = ['overdue'] if is_followup_view else []
|
||||
|
||||
sort_header = [
|
||||
{
|
||||
'key': column,
|
||||
'label': label,
|
||||
'link': sort_links[column],
|
||||
'active': sort_field == column,
|
||||
'dir': sort_dir if sort_field == column else '',
|
||||
}
|
||||
for column, label in ((key, label) for key, label, _ in columns)
|
||||
]
|
||||
# لینک «حذف همهٔ فیلترها»: نمای پیشفرض (امروز)، بدون تأخیر/جستوجو/مرتبسازی.
|
||||
clear_link = '?view=' + ENTEZAR_DEFAULT_VIEW
|
||||
has_filters = bool(
|
||||
delay or searchVal or sort_field != 'last_visit'
|
||||
or sort_dir != 'asc'
|
||||
# تاریخِ انتخابشده یا حالت «فقط عقبافتادهها» هم فیلتر است.
|
||||
or (is_followup_view and followup_date != today_date)
|
||||
or show_missed
|
||||
)
|
||||
|
||||
# اطلاعات هر ردیف برای قالب: قرارها + مدت تأخیر + آخرین بازدید مؤثر.
|
||||
appointment_rows = {}
|
||||
for customer in customerHa.object_list:
|
||||
entry = sources.get(customer.id) or {}
|
||||
entry = dict(sources.get(customer.id) or {})
|
||||
# در نمای پیگیری، تاریخ همان روز ملاک نمایش است (حتی اگر گذشته باشد و
|
||||
# در ``appointment_sources_map`` که فقط آینده را میبیند، نباشد).
|
||||
followup_entry = followups.get(customer.id) or {}
|
||||
entry.update(followup_entry)
|
||||
flags = [key for key in (SOURCE_MANUAL, SOURCE_SCHEDULED,
|
||||
SOURCE_COORDINATION) if key in entry]
|
||||
appointment_rows[customer.id] = {
|
||||
@@ -537,14 +820,36 @@ def entezar(request):
|
||||
'last_visit': visits.get(customer.id),
|
||||
# تعداد منابع (۰ تا ۳) — قالب بر اساس آن برچسب وضعیت را میسازد.
|
||||
'source_count': len(flags),
|
||||
# تاریخ پیگیری این نمای خاص (برای ستون اختصاصی نمای «امروز»).
|
||||
'followup_at': (min(followup_entry.values())
|
||||
if followup_entry else None),
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/entezar.html', {
|
||||
'customers': customerHa, 'searchStatus': searchStatus, 'full_path': full_path,
|
||||
'searchVal': searchVal, 'searchOn': searchOn,
|
||||
'view': view, 'counts': counts, 'appointment_rows': appointment_rows,
|
||||
'sort': sort, 'sorts': ENTEZAR_SORTS,
|
||||
'view_links': view_links, 'delay_links': delay_links,
|
||||
'sort_links': sort_links, 'sort_header': sort_header,
|
||||
'clear_link': clear_link,
|
||||
'has_filters': has_filters,
|
||||
'delay': delay, 'delays': ENTEZAR_DELAYS,
|
||||
'sort': sort_field, 'sort_dir': sort_dir,
|
||||
'sort_columns': ENTEZAR_SORT_COLUMNS,
|
||||
'table_columns': table_columns,
|
||||
'hidden_columns': hidden_columns,
|
||||
'column_count': len(table_columns),
|
||||
'page_prev': page_prev, 'page_next': page_next,
|
||||
'views': ENTEZAR_VIEWS,
|
||||
# نمای «نیازمند پیگیری»
|
||||
'is_followup_view': is_followup_view,
|
||||
'followup_view': ENTEZAR_FOLLOWUP_VIEW,
|
||||
'followup_date': followup_date,
|
||||
'followup_date_jalali': followup_date_jalali,
|
||||
'followup_nav': followup_nav,
|
||||
'show_missed': show_missed,
|
||||
'followup_overdue_link': followup_overdue_link,
|
||||
'followup_ontime_link': followup_ontime_link,
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user