Files
khatibi/admin_panel/templates/admin_panel/entezar.html
T
2026-10-21 12:24:52 +03:30

478 lines
25 KiB
HTML

{% extends "./base.html" %}
{% load custom_tags %}
{% block bodyPage %}
<style>
/* ===================================================================
صفحهٔ انتظار بازدید — mobile first
مقیاس فاصله: ۰٫۲۵rem (۴px) — همهٔ فاصله‌ها مضرب همین واحد است.
=================================================================== */
.ez-wrap { padding: .5rem; }
@media (min-width: 769px) { .ez-wrap { padding: 1rem 1.25rem; } }
/* ---------- نوار آمار ---------- */
.ez-stats-head { margin: 0 0 .55rem; }
.ez-stats-title { font-size: .92rem; font-weight: 800; color: #0f172a; margin: 0; }
.ez-stats-hint {
font-size: .72rem; color: #64748b; margin: .2rem 0 0; line-height: 1.7;
}
.ez-stats-hint b { color: #334155; }
.ez-stats {
display: grid; grid-auto-flow: column; grid-auto-columns: minmax(11rem, 1fr);
gap: .5rem; overflow-x: auto; padding-bottom: .35rem;
-webkit-overflow-scrolling: touch;
}
@media (min-width: 1024px) {
.ez-stats { grid-auto-flow: row; grid-template-columns: repeat(6, 1fr); overflow: visible; }
}
.ez-stat {
display: flex; flex-direction: column; gap: .18rem;
border: 1px solid #e2e8f0; border-radius: .625rem; background: #fff;
padding: .55rem .65rem .6rem; text-decoration: none; color: #334155;
border-top: 3px solid #cbd5e1;
transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease;
}
.ez-stat:hover { border-color: #94a3b8; box-shadow: 0 1px 6px rgba(15, 23, 42, .08); }
/* نوار رنگی بالای هر کارت = همان رنگ برچسب وضعیت در جدول */
.ez-stat.total { border-top-color: #64748b; }
.ez-stat.danger { border-top-color: #dc2626; }
.ez-stat.success { border-top-color: #16a34a; }
.ez-stat.warning { border-top-color: #d97706; }
.ez-stat.link { border-top-color: #0284c7; }
.ez-stat.violet { border-top-color: #7c3aed; }
.ez-stat-top {
display: flex; align-items: baseline; justify-content: space-between; gap: .4rem;
}
.ez-stat .ez-lbl {
font-size: .74rem; font-weight: 700; color: #334155; line-height: 1.35;
}
.ez-stat .ez-num {
font-size: 1.25rem; font-weight: 800; line-height: 1.1;
font-variant-numeric: tabular-nums; flex: 0 0 auto;
}
.ez-stat .ez-desc {
font-size: .68rem; color: #7c8798; line-height: 1.6;
}
.ez-stat.is-active {
border-color: #33b5d3; background: #f1fbff;
box-shadow: inset 0 -3px 0 #33b5d3, 0 1px 6px rgba(51, 181, 211, .18);
}
.ez-stat.is-active .ez-lbl { color: #0e7490; }
.ez-stat.is-active .ez-desc { color: #0e7490; }
/* ---------- تفکیک تأخیر ---------- */
.ez-legend {
display: flex; flex-wrap: wrap; gap: .25rem .9rem;
margin: .6rem 0 .5rem; font-size: .72rem; color: #64748b;
}
.ez-legend b { font-variant-numeric: tabular-nums; color: #334155; }
/* ---------- ابزارک‌ها ---------- */
.ez-toolbar {
display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: 0 0 .75rem;
}
.ez-toolbar .button { margin: 0 !important; }
.ez-seg {
display: inline-flex; border: 1px solid #e2e8f0; border-radius: .5rem; overflow: hidden;
}
.ez-seg a {
padding: .3rem .6rem; font-size: .72rem; color: #475569; text-decoration: none;
background: #fff; border-inline-end: 1px solid #e2e8f0; white-space: nowrap;
}
.ez-seg a:last-child { border-inline-end: 0; }
.ez-seg a:hover { background: #f1f5f9; }
.ez-seg a.is-active { background: #33b5d3; color: #fff; font-weight: 700; }
/* ---------- جدول ---------- */
.ez-table { width: 100%; margin: 0; font-size: .82rem; border-collapse: collapse; }
@media (min-width: 900px) { .ez-table { table-layout: fixed; } }
.ez-table thead th {
position: sticky; top: 0; z-index: 2; background: #33b5d3; color: #06323d;
font-size: .72rem; font-weight: 700; padding: .5rem .35rem; white-space: nowrap;
border: 0; border-bottom: 1px solid #22a5c1;
}
.ez-table tbody td {
padding: .45rem .3rem; vertical-align: middle; text-align: center;
line-height: 1.4; border: 0; border-bottom: 1px solid #eef2f6;
overflow-wrap: anywhere;
}
/* تاریخ‌ها و شماره‌ها هرگز به دو خط نشکنند */
.ez-table tbody td.ez-nowrap { white-space: nowrap; }
.ez-table tbody tr:hover td { background: #f8fbfd; }
/* شمارندهٔ ردیف: به‌جای یک ستون کامل، چیپ کنار نام */
.ez-idx {
display: inline-block; min-width: 1.55rem; margin-inline-end: .3rem;
padding: .05rem .25rem; border-radius: .3rem; background: #f1f5f9;
color: #64748b; font-size: .68rem; font-weight: 700;
font-variant-numeric: tabular-nums; text-align: center;
}
.ez-name { font-weight: 700; font-size: .84rem; color: #0f172a; }
.ez-name:hover { color: #0e7490; }
.ez-sub { font-size: .68rem; color: #94a3b8; margin-top: .1rem; }
/* اعداد و شماره‌ها */
.ez-num { font-variant-numeric: tabular-nums; }
.ez-phone {
direction: ltr; unicode-bidi: isolate;
font-variant-numeric: tabular-nums; letter-spacing: .01em;
}
.ez-muted { color: #cbd5e1; }
.ez-badge {
display: inline-block; padding: .1rem .45rem; border-radius: 999px;
font-size: .68rem; font-weight: 700; white-space: nowrap; line-height: 1.6;
}
.ez-badge.danger { background: #fee2e2; color: #b91c1c; }
.ez-badge.success { background: #dcfce7; color: #15803d; }
.ez-badge.warning { background: #fef3c7; color: #b45309; }
.ez-badge.mixed { background: #e0f2fe; color: #0369a1; }
.ez-badge.coordination { background: #ede9fe; color: #6d28d9; }
/* تأخیر به‌صورت چیپ (خوانا در یک نگاه) */
.ez-late {
display: inline-block; min-width: 4.4rem; padding: .1rem .4rem;
border-radius: .3rem; font-size: .72rem; font-weight: 700;
font-variant-numeric: tabular-nums;
}
.ez-late.lv0 { background: #f8fafc; color: #94a3b8; }
.ez-late.lv1 { background: #fffbeb; color: #b45309; }
.ez-late.lv2 { background: #fef2f2; color: #dc2626; }
.ez-late.lv3 { background: #fde8e8; color: #991b1b; }
/* آدرس: حداکثر دو خط (بدون شکستن چیدمان) */
.ez-addr {
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; text-align: right; font-size: .78rem; color: #475569;
}
/* ---------- حالت کارت (موبایل) ---------- */
.ez-only-desktop { display: none !important; }
@media (max-width: 899px) {
.ez-table thead { display: none; }
.ez-table, .ez-table tbody, .ez-table tr, .ez-table td { display: block; width: 100%; }
.ez-table tbody tr {
display: flex; flex-wrap: wrap; align-items: baseline;
background: #fff; border: 1px solid #e5e7eb; border-radius: .625rem;
margin-bottom: .5rem; padding: .55rem .7rem;
}
.ez-table tbody tr:hover td { background: transparent; }
.ez-table tbody td {
flex: 1 0 100%; display: flex; flex-wrap: wrap; align-items: baseline;
gap: .3rem; padding: .12rem 0; border: 0; text-align: right;
}
.ez-table tbody td::before {
content: attr(data-label); flex: 0 0 6.5rem;
font-size: .7rem; color: #94a3b8;
}
/* سرِ کارت: نام مشتری، بدون برچسب */
.ez-table tbody td.ez-card-head {
padding-bottom: .35rem; margin-bottom: .25rem;
border-bottom: 1px dashed #eef2f6;
}
.ez-table tbody td.ez-card-head::before,
.ez-table tbody td.ez-nolabel::before { content: none; }
.ez-actions { flex: 1 0 100%; display: flex; gap: .35rem; margin-top: .4rem; }
.ez-actions .button { margin: 0 !important; flex: 1 1 0; }
.ez-addr { -webkit-line-clamp: 3; }
.ez-legend { font-size: .7rem; }
}
@media (min-width: 900px) {
.ez-table thead { display: table-header-group; }
.ez-table tbody { display: table-row-group; }
.ez-table tr { display: table-row; }
.ez-table td { display: table-cell; }
.ez-table td::before { content: none; }
.ez-only-desktop { display: table-cell !important; }
.ez-only-mobile { display: none !important; }
.ez-actions { display: none; }
}
</style>
<div class="ez-wrap">
<div class="columns m-0">
<div class="column is-12 p-0">
{# ===== نوار آمار: هر عدد لینک فیلتر ===== #}
<div class="ez-stats-head">
<p class="ez-stats-title">وضعیت مشتریانِ عقب‌افتاده</p>
<p class="ez-stats-hint">
«عقب‌افتاده» یعنی از آخرین بازدید
<b>بیش از {{ counts.period_months }} ماه</b> گذشته باشد.
روی هر کارت بزنید تا فقط همان گروه نمایش داده شود؛
<b>کل = بدون قرار + با قرار</b>.
سه کارتِ آخر سه نوع قرارِ جدا هستند؛ یک مشتری می‌تواند بیش از
یکی داشته باشد، پس جمع آن‌ها ممکن است از «با قرار» بیشتر شود.
</p>
</div>
<div class="ez-stats">
<a href="?view=all&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
class="ez-stat total {% if view == 'all' %}is-active{% endif %}"
title="همهٔ مشتریان عقب‌افتاده، با هر وضعیت قرار">
<span class="ez-stat-top">
<span class="ez-lbl">کل عقب‌افتاده‌ها</span>
<span class="ez-num">{{ counts.total }}</span>
</span>
<span class="ez-desc">همه، فارغ از داشتن یا نداشتن قرار</span>
</a>
<a href="?view=uncoordinated&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
class="ez-stat danger {% if view == 'uncoordinated' %}is-active{% endif %}"
title="مشتریانی که هنوز هیچ قرار بعدی برایشان ثبت نشده است">
<span class="ez-stat-top">
<span class="ez-lbl">بدون قرار</span>
<span class="ez-num has-text-danger">{{ counts.uncoordinated }}</span>
</span>
<span class="ez-desc">هنوز وقت بازدید تعیین نشده — نیاز به تماس</span>
</a>
<a href="?view=coordinated&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
class="ez-stat success {% if view == 'coordinated' %}is-active{% endif %}"
title="مشتریانی که وقت بازدید بعدی برایشان مشخص شده است">
<span class="ez-stat-top">
<span class="ez-lbl">با قرار</span>
<span class="ez-num has-text-success">{{ counts.coordinated }}</span>
</span>
<span class="ez-desc">وقت بازدیدشان مشخص است (هر دو نوع قرار)</span>
</a>
<a href="?view=manual&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
class="ez-stat warning {% if view == 'manual' %}is-active{% endif %}"
title="قرار تعویق که از فرم «هماهنگی» ثبت شده است">
<span class="ez-stat-top">
<span class="ez-lbl">قرار هماهنگی</span>
<span class="ez-num has-text-warning-dark">{{ counts.manual }}</span>
</span>
<span class="ez-desc">تعویق دستی از فرم هماهنگی (مثلاً «ماه بعد زنگ بزنید»)</span>
</a>
<a href="?view=scheduled&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
class="ez-stat link {% if view == 'scheduled' %}is-active{% endif %}"
title="اقدام «بازدید» با تاریخ آینده">
<span class="ez-stat-top">
<span class="ez-lbl">بازدید برنامه‌ریزی‌شده</span>
<span class="ez-num has-text-link">{{ counts.scheduled }}</span>
</span>
<span class="ez-desc">اقدام «بازدید» با تاریخ آینده ثبت شده است</span>
</a>
<a href="?view=coordination&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
class="ez-stat violet {% if view == 'coordination' %}is-active{% endif %}"
title="اقدام «هماهنگی» با تاریخ پیگیری در آینده">
<span class="ez-stat-top">
<span class="ez-lbl">هماهنگی</span>
<span class="ez-num" style="color:#7c3aed">{{ counts.coordination }}</span>
</span>
<span class="ez-desc">تماس گرفته شده و پیگیری به آینده موکول شده است</span>
</a>
</div>
{# تفکیک مدت تأخیر در نمای جاری #}
<div class="ez-legend">
<span>میزان تأخیر در همین نما:</span>
<span><b>{{ counts.buckets.lt30 }}</b> تا ۳۰ روز</span>
<span><b>{{ counts.buckets.30to90 }}</b> ۳۰ تا ۹۰ روز</span>
<span><b>{{ counts.buckets.90to180 }}</b> ۹۰ تا ۱۸۰ روز</span>
<span><b>{{ counts.buckets.gt180 }}</b> بیش از ۶ ماه</span>
</div>
{# ===== ابزارک‌ها: جستجو و مرتب‌سازی ===== #}
<div class="ez-toolbar">
<a href="#" id="openSearchBox" class="button is-info is-outlined is-rounded is-small">
جستجو{% if searchVal %}: «{{ searchVal }}»{% endif %}
</a>
<span class="is-size-7 has-text-grey">مرتب‌سازی</span>
<span class="ez-seg">
<a href="?view={{ view }}&sort=oldest{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
class="{% if sort == 'oldest' %}is-active{% endif %}">قدیمی‌ترین</a>
<a href="?view={{ view }}&sort=overdue{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
class="{% if sort == 'overdue' %}is-active{% endif %}">بیشترین تأخیر</a>
<a href="?view={{ view }}&sort=newest{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
class="{% if sort == 'newest' %}is-active{% endif %}">جدیدترین</a>
</span>
{% if searchVal %}
<a href="?view={{ view }}&sort={{ sort }}" class="button is-small is-light">حذف جستجو</a>
{% endif %}
</div>
<div class="table-container">
<table class="table ez-table">
<colgroup>
<col style="width: 21%">
<col style="width: 12%">
<col style="width: 10%">
<col style="width: 10%">
<col style="width: 10%">
<col style="width: 9%">
<col style="width: 8%">
<col style="width: 9%">
<col style="width: 11%">
</colgroup>
<thead>
<tr>
<th>نام و نام خانوادگی</th>
<th>وضعیت قرار</th>
<th>قرار دستی</th>
<th>بازدید برنامه‌ریزی‌شده</th>
<th>هماهنگی</th>
<th>آخرین بازدید</th>
<th>تأخیر</th>
<th>تلفن</th>
<th>آدرس</th>
</tr>
</thead>
<tbody>
{% for customer in customers %}
<tr>
{# سرِ ردیف: چیپ شماره + نام (شمارهٔ ردیف دیگر ستون جداگانه ندارد) #}
<td data-label="نام" class="ez-card-head" style="text-align: right;">
<input class="is-hidden fid fidv-20" value="20">
<span class="ez-idx">{{ forloop.counter0|add:customers.start_index }}</span>
<a target="_blank" href="{% url 'admin_panel:customerAction' customer.id %}" class="tdvl name ez-name">
{% if customer.name_family|length >= 2 %}{{ customer.name_family }}{% else %}نامعلوم{% endif %}
</a>
{% if customer.idCode %}<div class="ez-sub">کد: {{ customer.idCode }}</div>{% endif %}
</td>
{% with appt=appointment_rows|row_for:customer.id %}
<td data-label="وضعیت" class="ez-nolabel">
{% if appt.source_count > 1 %}
<span class="ez-badge mixed">{{ appt.source_count }} قرار</span>
{% elif appt.manual %}
<span class="ez-badge warning">قرار دستی</span>
{% elif appt.scheduled %}
<span class="ez-badge success">بازدید برنامه</span>
{% elif appt.coordination %}
<span class="ez-badge coordination">هماهنگی</span>
{% else %}
<span class="ez-badge danger">بدون قرار</span>
{% endif %}
</td>
<td data-label="قرار دستی" class="ez-num ez-nowrap">
{% if appt.manual %}{{ appt.manual|toJalali }}{% else %}<span class="ez-muted">—</span>{% endif %}
</td>
<td data-label="بازدید برنامه" class="ez-num ez-nowrap">
{% if appt.scheduled %}{{ appt.scheduled|toJalali }}{% else %}<span class="ez-muted">—</span>{% endif %}
</td>
<td data-label="هماهنگی" class="ez-num ez-nowrap">
{% if appt.coordination %}{{ appt.coordination|toJalali }}{% else %}<span class="ez-muted">—</span>{% endif %}
</td>
<td data-label="آخرین بازدید" class="ez-num ez-nowrap lastView">
{% if appt.last_visit %}{{ appt.last_visit|toJalali }}{% else %}<span class="ez-muted">بدون بازدید</span>{% endif %}
</td>
<td data-label="تأخیر" class="ez-nowrap">
{% if appt.overdue_days >= 180 %}
<span class="ez-late lv3">{{ appt.overdue_days }} روز</span>
{% elif appt.overdue_days >= 90 %}
<span class="ez-late lv2">{{ appt.overdue_days }} روز</span>
{% elif appt.overdue_days >= 30 %}
<span class="ez-late lv1">{{ appt.overdue_days }} روز</span>
{% else %}
<span class="ez-late lv0">{{ appt.overdue_days }} روز</span>
{% endif %}
</td>
{% endwith %}
<td data-label="تلفن" class="ez-nowrap">
<span class="tdvl mobile_1 ez-phone">
{% if customer.phone1 %}{{ customer.phone1 }}{% else %}<span class="ez-muted">—</span>{% endif %}
</span>
<span class="tdvl is-hidden mobile_2">{{ customer.phone2 }}</span>
<span class="tdvl is-hidden mobile_3">{{ customer.phone3 }}</span>
{# دکمه‌های اقدام سریع — فقط موبایل #}
<div class="ez-actions">
{% if customer.phone1 %}
<a href="tel:{{ customer.phone1 }}" class="button is-success is-small">تماس</a>
{% endif %}
<a target="_blank" href="{% url 'admin_panel:customerAction' customer.id %}" class="button is-info is-small">ثبت اقدام</a>
</div>
</td>
<td data-label="آدرس">
<div class="tdvl address ez-addr" title="{{ customer.address }}">{{ customer.address }}</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="has-text-centered py-5">
<span class="has-text-grey">موردی در این نما پیدا نشد.</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# ===== صفحه‌بندی ===== #}
<div class="is-flex is-align-items-center is-justify-content-space-between mt-3 mb-5">
{% if customers.has_previous %}
<a class="button is-danger is-small" href="?view={{ view }}&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}&page={{ customers.previous_page_number }}">صفحه قبل</a>
{% else %}<span></span>{% endif %}
<span class="is-size-7 has-text-grey">
صفحهٔ {{ customers.number }} از {{ customers.paginator.num_pages }} ·
{{ customers.paginator.count }} مورد
</span>
{% if customers.has_next %}
<a class="button is-danger is-small" href="?view={{ view }}&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}&page={{ customers.next_page_number }}">صفحه بعد</a>
{% else %}<span></span>{% endif %}
</div>
</div>
</div>
</div>
<div class="modal SearchBox">
<div class="modal-background closeSearch"></div>
<div class="modal-content">
<form method="get" class="columns is-mobile is-multiline">
{# حفظ نما و ترتیب هنگام جست‌وجو #}
<input type="hidden" name="view" value="{{ view }}">
<input type="hidden" name="sort" value="{{ sort }}">
<div class="field column is-7-desktop is-12-mobile m-0">
<label class="label">جستجو کنید</label>
<div class="control">
<input name="searchVal" class="input search" type="text" value="{{ searchVal }}" placeholder="نام، تلفن، کد یا آدرس">
</div>
</div>
<div class="field column is-5-desktop is-12-mobile m-0">
<label class="label">فیلد</label>
<div class="select is-fullwidth">
<select name="searchOn">
<option value="name" {% if searchOn == 'name' %}selected{% endif %}>نام و نام خانوادگی</option>
<option value="phone" {% if searchOn == 'phone' %}selected{% endif %}>تلفن</option>
<option value="address" {% if searchOn == 'address' %}selected{% endif %}>آدرس</option>
<option value="code" {% if searchOn == 'code' %}selected{% endif %}>کد مشتری</option>
</select>
</div>
</div>
<div class="field column is-12-desktop is-12-mobile m-0">
<input class="button is-success is-fullwidth" id="searchbtn" value="جستجو مشتری" type="submit">
</div>
</form>
</div>
<button class="modal-close is-large closeSearch" aria-label="close"></button>
</div>
{% endblock %}
{% block jsEnd %}
<script>
$(document).ready(function() {
$("#openSearchBox").click(function(e) {
e.preventDefault();
$(".SearchBox").addClass("is-active");
});
$(".closeSearch").click(function() {
$(".SearchBox").removeClass("is-active");
});
});
</script>
{% endblock %}