some improve

This commit is contained in:
javad committed 2026-10-27 11:49:09 +03:30
1 parent fa73a734a0
commit 2e5d0bcdd3
17 files changed
+1512 -226

No files matched your search

@@ -1,40 +0,0 @@
{% if perms.admin_panel.ManageSMS %}
<div class="box" dir="rtl">
<h2 class="title is-5">پیامک</h2>
<div class="columns is-multiline">
<div class="column is-12">
<h3 class="subtitle is-6">فعال/غیرفعال پیامک‌ها برای این مشتری</h3>
{% for item in sms_toggles %}
<form method="post" action="{% url 'admin_panel:sms_customer_toggle' customer.id %}" class="field has-addons">
{% csrf_token %}
<input type="hidden" name="sms_type" value="{{ item.code }}">
<input type="hidden" name="is_enabled" value="{% if item.enabled %}false{% else %}true{% endif %}">
<div class="control is-expanded">
<input class="input" type="text" value="{{ item.label }}" disabled>
</div>
<div class="control">
<button class="button {% if item.enabled %}is-success{% else %}is-danger{% endif %}" type="submit">
{% if item.enabled %}فعال{% else %}غیرفعال{% endif %}
</button>
</div>
</form>
{% endfor %}
</div>
<div class="column is-12">
<h3 class="subtitle is-6">ارسال پیامک سفارشی</h3>
<form method="post" action="{% url 'admin_panel:sms_send_custom' customer.id %}">
{% csrf_token %}
<div class="field">
<div class="control">
<textarea class="textarea" name="text" rows="2" placeholder="متن پیامک..."></textarea>
</div>
</div>
<p class="help">می‌توانید از متغیرهایی مثل {name} (نام مشتری) استفاده کنید.</p>
<button class="button is-info" type="submit">ارسال به شماره‌های اصلی</button>
</form>
</div>
</div>
</div>
{% endif %}
@@ -180,9 +180,12 @@
</form>
</div>
{% include "./_customer_sms.html" %}
</div>
{% comment %}
باکس «پیامک» از این صفحه حذف شد: تنظیمات پیامک به تب «سوابق پیامک» مشتری
(/panel/customer/<id>/sms/) منتقل شده تا فرم فاکتور تمام‌عرض بماند و سوابق و
تنظیمات هر دو در یک صفحهٔ اختصاصی دیده شوند.
{% endcomment %}
{% endblock %}
@@ -4,30 +4,223 @@
{% block BodyCustomer1 %}
{% comment %}
صفحهٔ «پیامک مشتری» = سوابق + تنظیمات در یک صفحه.
قاعدهٔ طراحی: تنظیمات هرگز روی سوابق نمی‌نشیند؛ فقط آمار + دکمه‌ها بالاست و
تنظیمات/ارسال سفارشی/شماره‌های اصلی داخل مودال باز می‌شوند.
همهٔ کلاس‌ها با پیشوند csmsx و محدود به #customerSmsHistory هستند تا به
صفحات دیگر نشت نکنند.
{% endcomment %}
<style>
/* جدول سوابق پیامک مشتری */
.smshist-msg {
white-space: pre-wrap;
overflow-wrap: anywhere;
max-width: 34rem;
line-height: 1.9;
{% verbatim %}
#customerSmsHistory, #customerSmsHistory *, #customerSmsHistory *::before,
#customerSmsHistory *::after { box-sizing: border-box; }
/* متغیرها روی خودِ صفحه و روی ریشهٔ مودال‌ها تعریف می‌شوند؛ مودال‌ها در بلوک
جدا (BodyCustomer2) رندر می‌شوند و خارج از #customerSmsHistory هستند. */
#customerSmsHistory, .csmsx-modal { --cs-line: #e8ecf3; --cs-ink: #363636;
--cs-muted: #7a7a7a; --cs-brand: #3273dc; --cs-ok: #48c774; }
/* ---------- نوار بالای صفحه ---------- */
.csmsx-head { display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-start;
justify-content: space-between; padding: 1.1rem 1.2rem; margin-bottom: 1rem;
border: 1px solid var(--cs-line); border-radius: 14px;
background: linear-gradient(180deg, #fbfdff 0%, #f5f8fd 100%); }
.csmsx-head__main { min-width: 0; }
.csmsx-title { font-size: 1.15rem; font-weight: 700; color: var(--cs-ink);
margin: 0 0 .25rem; overflow-wrap: anywhere; }
.csmsx-sub { margin: 0; color: var(--cs-muted); font-size: .82rem; line-height: 1.8; }
.csmsx-sub b { color: var(--cs-ink); }
.csmsx-stats { display: flex; flex-wrap: wrap; gap: .45rem; margin-top: .7rem; }
.csmsx-stat { display: inline-flex; align-items: center; gap: .35rem;
font-size: .78rem; padding: .25rem .65rem; border-radius: 999px;
background: #fff; border: 1px solid var(--cs-line); color: #4a4a4a; white-space: nowrap; }
.csmsx-stat b { font-variant-numeric: tabular-nums; }
.csmsx-stat.is-sent { border-color: #c8ebd5; background: #f2fbf6; }
.csmsx-stat.is-pending { border-color: #ffeaa7; background: #fffdf3; }
.csmsx-stat.is-failed { border-color: #ffd0d9; background: #fff5f7; }
.csmsx-head__actions { display: flex; flex-wrap: wrap; gap: .5rem; }
{% endverbatim %}
</style>
<style>
{% verbatim %}
/* ---------- نوار فیلترها ---------- */
.csmsx-filters { display: flex; flex-wrap: wrap; gap: .6rem; align-items: flex-end;
padding: .85rem 1rem; margin-bottom: 1rem; background: #fff;
border: 1px solid var(--cs-line); border-radius: 12px; }
.csmsx-filters .control { margin: 0; }
.csmsx-filters label { display: block; font-size: .75rem; color: var(--cs-muted);
margin-bottom: .25rem; }
.csmsx-filters select { min-width: 10.5rem; }
.csmsx-filters__note { margin-inline-start: auto; font-size: .78rem;
color: var(--cs-muted); }
/* ---------- فهرست سوابق ---------- */
.csmsx-history { background: #fff; border: 1px solid var(--cs-line);
border-radius: 14px; overflow: hidden; }
.csmsx-history__scroll { overflow-x: auto; }
.csmsx-history table { margin: 0 !important; background: transparent; }
.csmsx-history thead th { position: sticky; top: 0; z-index: 1; background: #f7f9fc;
color: #4a4a4a; font-size: .78rem; font-weight: 700;
border-bottom: 1px solid var(--cs-line) !important; }
.csmsx-history tbody tr:hover { background: #fafcff; }
.csmsx-msg { white-space: pre-wrap; overflow-wrap: anywhere; max-width: 34rem;
line-height: 1.95; font-size: .84rem; }
/* پیامک‌های بلند (متن قالب‌ها چندخطی است) کل جدول را بی‌اندازه بلند می‌کردند؛
متن در ۴ خط خلاصه می‌شود و با «نمایش کامل» باز می‌شود. */
.csmsx-msg.is-clamped { display: -webkit-box; -webkit-line-clamp: 4;
-webkit-box-orient: vertical; overflow: hidden; }
.csmsx-expand { margin-top: .25rem; font-size: .72rem; padding: .1rem .45rem;
border-radius: 6px; border: 1px solid var(--cs-line); background: #fff;
color: var(--cs-brand); cursor: pointer; }
.csmsx-expand:hover { background: #f4f8ff; }
.csmsx-meta { font-size: .76rem; line-height: 1.7; color: var(--cs-muted); }
.csmsx-badge { white-space: nowrap; }
.csmsx-type-chip { display: inline-block; font-size: .75rem; padding: .1rem .5rem;
border-radius: 999px; background: #f1f5fb; color: #33507d; white-space: nowrap; }
{% endverbatim %}
</style>
<style>
{% verbatim %}
/* ---------- مودال‌ها ---------- */
.csmsx-modal .modal-content { max-width: 46rem; width: calc(100% - 1.5rem); }
.csmsx-modal .box { padding: 0; overflow: hidden; border-radius: 14px; }
.csmsx-modal__head { display: flex; align-items: flex-start; justify-content: space-between;
gap: .75rem; padding: 1rem 1.15rem; border-bottom: 1px solid var(--cs-line);
background: linear-gradient(180deg, #fbfdff 0%, #f4f8fd 100%); }
.csmsx-modal__title { margin: 0; font-size: 1.02rem; font-weight: 700; color: var(--cs-ink); }
.csmsx-modal__sub { margin: .2rem 0 0; font-size: .78rem; color: var(--cs-muted); line-height: 1.8; }
.csmsx-modal__body { padding: 1rem 1.15rem; max-height: min(68vh, 34rem); overflow-y: auto; }
.csmsx-modal__foot { display: flex; flex-wrap: wrap; gap: .5rem; justify-content: flex-end;
padding: .85rem 1.15rem; border-top: 1px solid var(--cs-line); background: #fcfcfd; }
.csmsx-section-title { display: flex; align-items: center; gap: .45rem; margin: 0 0 .6rem;
font-size: .82rem; font-weight: 700; color: #4a4a4a; }
.csmsx-section + .csmsx-section { margin-top: 1.15rem; padding-top: 1.15rem;
border-top: 1px dashed var(--cs-line); }
/* ردیف هر نوع پیامک داخل مودال تنظیمات */
.csmsx-type { display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
gap: .6rem .75rem; align-items: center; padding: .6rem .7rem; border-radius: 10px; }
.csmsx-type + .csmsx-type { margin-top: .3rem; }
.csmsx-type:hover { background: #fafcff; }
.csmsx-type.is-off { background: #fcfcfc; }
.csmsx-type.is-off .csmsx-type__label { color: #9a9a9a; }
.csmsx-type__icon { font-size: 1.15rem; line-height: 1; }
.csmsx-type__main { min-width: 0; }
.csmsx-type__label { display: block; font-size: .85rem; font-weight: 600;
color: var(--cs-ink); overflow-wrap: anywhere; }
.csmsx-type__meta { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .2rem; }
.csmsx-tag { font-size: .7rem; padding: .1rem .45rem; border-radius: 999px;
border: 1px solid var(--cs-line); background: #fff; color: var(--cs-muted); white-space: nowrap; }
.csmsx-tag.is-auto { border-color: #cfe0ff; background: #f4f8ff; color: #2f5fb3; }
/* کلید فعال/غیرفعال (دکمهٔ submit داخل فرم) */
.csmsx-switch { display: inline-flex; align-items: center; gap: .45rem;
background: none; border: 0; padding: .25rem; cursor: pointer; min-height: 2.4rem; }
.csmsx-switch__track { position: relative; flex: 0 0 auto; width: 44px; height: 25px;
border-radius: 13px; background: #dbdbdb; transition: background .18s; }
.csmsx-switch__track::after { content: ""; position: absolute; top: 3px;
inset-inline-start: 3px; width: 19px; height: 19px; border-radius: 50%;
background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3); transition: transform .18s; }
.csmsx-switch.is-on .csmsx-switch__track { background: var(--cs-ok); }
.csmsx-switch.is-on .csmsx-switch__track::after { transform: translateX(-19px); }
.csmsx-switch:focus-visible .csmsx-switch__track { outline: 2px solid var(--cs-brand); outline-offset: 2px; }
.csmsx-switch__text { font-size: .76rem; color: #4a4a4a; white-space: nowrap; min-width: 3.2rem; text-align: start; }
/* شماره‌های اصلی داخل مودال */
.csmsx-phones { display: flex; flex-wrap: wrap; gap: .5rem; }
.csmsx-phone { display: inline-flex; align-items: center; gap: .4rem; padding: .3rem .4rem .3rem .65rem;
border: 1px solid var(--cs-line); border-radius: 999px; background: #fff; }
.csmsx-phone__num { direction: ltr; font-size: .82rem; color: var(--cs-ink); }
.csmsx-phone .sms-primary-toggle { border-radius: 999px; font-size: .72rem; height: 1.9rem; padding-inline: .6rem; }
.csmsx-empty { font-size: .8rem; color: var(--cs-muted); margin: 0; }
/* ارسال سفارشی */
.csmsx-chips { display: flex; flex-wrap: wrap; gap: .35rem; margin-bottom: .5rem; }
.csmsx-chip { font-size: .72rem; padding: .2rem .55rem; border-radius: 999px;
border: 1px solid var(--cs-line); background: #fff; color: #4a4a4a; cursor: pointer;
direction: ltr; font-family: ui-monospace, Menlo, Consolas, monospace; }
.csmsx-chip:hover { border-color: var(--cs-brand); color: var(--cs-brand); }
.csmsx-counter { display: flex; justify-content: space-between; gap: .5rem;
font-size: .74rem; color: var(--cs-muted); margin-top: .4rem; }
/* ---------- واکنش‌گرا ---------- */
@media (max-width: 768px) {
.csmsx-head { padding: .9rem; }
.csmsx-head__actions { width: 100%; }
.csmsx-head__actions .button { flex: 1 1 auto; }
.csmsx-filters { padding: .75rem; }
.csmsx-filters select { min-width: 0; width: 100%; }
.csmsx-filters .control { flex: 1 1 45%; }
.csmsx-filters__note { margin-inline-start: 0; width: 100%; }
.csmsx-modal__body { max-height: 72vh; }
/* موبایل: جدول به کارت تبدیل می‌شود. اسکرول افقی روی موبایل، متن پیامک را
می‌بُرد و خواندن سوابق را عملاً غیرممکن می‌کرد؛ در این حالت هر پیامک یک
کارت کامل با برچسب هر خانه است و هیچ اسکرول افقی لازم نیست. */
.csmsx-history__scroll { overflow: visible; }
.csmsx-history table, .csmsx-history tbody, .csmsx-history tr,
.csmsx-history td { display: block; width: auto !important; }
.csmsx-history thead { display: none; }
.csmsx-history tbody tr { padding: .7rem .85rem; border-bottom: 1px solid var(--cs-line); }
.csmsx-history tbody tr:last-child { border-bottom: 0; }
.csmsx-history tbody td { display: flex; gap: .5rem; align-items: flex-start;
padding: .18rem 0 !important; border: 0 !important; }
.csmsx-history tbody td::before { content: attr(data-label); flex: 0 0 5.5rem;
font-size: .72rem; color: var(--cs-muted); line-height: 1.9; }
.csmsx-history tbody td[data-label=""]::before { content: none; }
/* ستون متن: برچسب بالای متن (نه کنارش) تا عرض خواندن کم نشود. */
.csmsx-history tbody td[data-label="متن"] { display: block; }
.csmsx-history tbody td[data-label="متن"]::before { display: block;
flex: none; margin-bottom: .15rem; }
.csmsx-history tbody td > * { min-width: 0; }
.csmsx-expand { white-space: nowrap; }
.csmsx-msg { max-width: none; }
.csmsx-col-id, .csmsx-col-sent, .csmsx-col-delivery-check { display: none !important; }
}
.smshist-meta { font-size: .78rem; line-height: 1.7; }
.smshist-badge { white-space: nowrap; }
{% endverbatim %}
</style>
<div id="customerSmsHistory" class="column is-12-mobile is-12-desktop" dir="rtl">
<h1 class="has-text-centered has-text-weight-bold mb-1">سوابق پیامک مشتری</h1>
<p class="has-text-centered has-text-grey smshist-meta mb-3">
همهٔ پیامک‌های ارسال‌شده برای شماره‌های این مشتری — جمعاً
<strong>{{ total_count }}</strong> پیامک.
</p>
<form method="get" class="field is-grouped is-grouped-multiline mb-3">
{# ---------- ۱) نوار بالای صفحه: آمار + دکمه‌های تنظیمات ---------- #}
<div class="csmsx-head">
<div class="csmsx-head__main">
<h1 class="csmsx-title">پیامک‌های {{ customer.name_family }}</h1>
<p class="csmsx-sub">
جمعاً <b>{{ total_count }}</b> پیامک در سابقه ·
<b>{{ sms_enabled_count }}</b> از <b>{{ sms_type_total }}</b> نوع پیامک برای این مشتری فعال است.
</p>
<div class="csmsx-stats">
<span class="csmsx-stat is-sent">✅ ارسال‌شده <b>{{ stat_sent }}</b></span>
<span class="csmsx-stat is-pending">⏳ در صف <b>{{ stat_pending }}</b></span>
<span class="csmsx-stat is-failed">⚠️ ناموفق <b>{{ stat_failed }}</b></span>
</div>
</div>
<div class="csmsx-head__actions">
{% if perms.admin_panel.ManageSMS %}
<button type="button" class="button is-link is-light js-csmsx-open" data-target="smsSettingsModal"
aria-haspopup="dialog">
⚙️ تنظیمات پیامک
</button>
<button type="button" class="button is-info is-light js-csmsx-open" data-target="smsCustomModal"
aria-haspopup="dialog">
✉️ ارسال پیامک
</button>
{% endif %}
{% if perms.admin_panel.ManageSMSSettings %}
<a class="button is-white" href="{% url 'admin_panel:sms_templates' %}"
title="ویرایش متن قالب‌های پیامک">🗂️ متن قالب‌ها</a>
{% endif %}
</div>
</div>
{# ---------- ۲) فیلترها ---------- #}
<form method="get" class="csmsx-filters">
<div class="control">
<label for="csmsx-status">وضعیت ارسال</label>
<div class="select">
<select name="status">
<option value="">همهٔ وضعیت‌های ارسال</option>
<select id="csmsx-status" name="status">
<option value="">همهٔ وضعیت‌ها</option>
{% for value, label in status_choices %}
<option value="{{ value }}" {% if status == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
@@ -35,88 +228,113 @@
</div>
</div>
<div class="control">
<label for="csmsx-delivery">وضعیت تحویل</label>
<div class="select">
<select name="delivery">
<option value="">همهٔ وضعیت‌های تحویل</option>
<select id="csmsx-delivery" name="delivery">
<option value="">همهٔ وضعیت‌ها</option>
{% for value, label in delivery_choices %}
<option value="{{ value }}" {% if delivery == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="control"><button class="button is-link" type="submit">فیلتر</button></div>
<div class="control">
<button class="button is-link" type="submit">اعمال فیلتر</button>
</div>
{% if status or delivery %}
<div class="control">
<a class="button is-light" href="{% url 'admin_panel:customerSmsHistory' customer.id %}">حذف فیلتر</a>
</div>
{% endif %}
<span class="csmsx-filters__note">
{% if status or delivery %}
نمایش <b>{{ filtered_count }}</b> از <b>{{ total_count }}</b> پیامک
{% else %}
نمایش <b>{{ total_count }}</b> پیامک، جدیدترین‌ها ابتدا
{% endif %}
</span>
</form>
{% if status or delivery %}
<p class="mb-2 smshist-meta">نتایج فیلترشده: <strong>{{ filtered_count }}</strong> مورد</p>
{% endif %}
<div class="table-container">
<table class="table is-fullwidth is-narrow is-hoverable is-striped is-bordered">
<thead>
<tr>
<th>شناسه</th>
<th style="min-width: 7rem;">نوع پیامک</th>
<th style="min-width: 8rem;">شماره گیرنده</th>
<th style="min-width: 18rem;">متن پیامک</th>
<th>وضعیت ارسال</th>
<th style="min-width: 8rem;">وضعیت تحویل</th>
<th>تاریخ ایجاد</th>
<th>تاریخ ارسال</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.get_sms_type_display|default:'—' }}</td>
<td dir="ltr">{{ item.phone_number }}</td>
<td class="smshist-msg">{{ item.message }}</td>
<td>
<span class="tag smshist-badge
{% if item.status == 'sent' %}is-success
{% elif item.status == 'failed' %}is-danger
{% elif item.status == 'pending' %}is-warning
{% elif item.status == 'approved' %}is-info
{% else %}is-light{% endif %}">
{{ item.get_status_display }}
</span>
{% if item.last_error %}
<div class="has-text-danger smshist-meta" title="{{ item.last_error }}">
خطا: {{ item.last_error|truncatechars:80 }}
</div>
{% endif %}
</td>
<td>
<span class="tag smshist-badge
{% if item.delivery_status == 'delivered' %}is-success
{% elif item.delivery_status == 'not_delivered' %}is-danger
{% elif item.delivery_status == 'expired' %}is-danger
{% elif item.delivery_status == 'pending' %}is-warning
{% else %}is-light{% endif %}">
{{ item.get_delivery_status_display|default:'بدون پیگیری' }}
</span>
{% if item.delivery_checked_at %}
<div class="has-text-grey smshist-meta">استعلام: {{ item.delivery_checked_at|toJalaliDT }}</div>
{% endif %}
</td>
<td class="smshist-meta">{{ item.created_at|toJalaliDT }}</td>
<td class="smshist-meta">{{ item.sent_at|toJalaliDT|default:'—' }}</td>
</tr>
{% empty %}
<tr><td colspan="8" class="has-text-centered">پیامکی برای این مشتری ثبت نشده است.</td></tr>
{% endfor %}
</tbody>
</table>
{# ---------- ۳) سوابق ---------- #}
<div class="csmsx-history">
<div class="csmsx-history__scroll">
<table class="table is-fullwidth is-narrow is-hoverable is-striped">
<thead>
<tr>
<th class="csmsx-col-id">شناسه</th>
<th style="min-width: 8.5rem;">نوع پیامک</th>
<th style="min-width: 8rem;">گیرنده</th>
<th style="min-width: 16rem;">متن پیامک</th>
<th>وضعیت ارسال</th>
<th style="min-width: 8rem;">وضعیت تحویل</th>
<th style="min-width: 7rem;">تاریخ ایجاد</th>
<th class="csmsx-col-sent" style="min-width: 7rem;">تاریخ ارسال</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td class="csmsx-col-id csmsx-meta" data-label="شناسه">{{ item.id }}</td>
<td data-label="نوع">
{% if item.sms_type %}
<span class="csmsx-type-chip">{{ item.get_sms_type_display }}</span>
{% else %}
<span class="csmsx-type-chip">سفارشی</span>
{% endif %}
{% if item.attempts %}
<div class="csmsx-meta">تلاش: {{ item.attempts }}</div>
{% endif %}
</td>
<td class="csmsx-meta" dir="ltr" data-label="گیرنده">{{ item.phone_number }}</td>
<td data-label="متن">
<div class="csmsx-msg {% if item.message|length > 170 %}is-clamped{% endif %}">{{ item.message }}</div>
{% if item.message|length > 170 %}
<button type="button" class="csmsx-expand js-csmsx-expand" aria-expanded="false">نمایش کامل</button>
{% endif %}
</td>
<td data-label="ارسال">
<span class="tag csmsx-badge
{% if item.status == 'sent' %}is-success
{% elif item.status == 'failed' %}is-danger
{% elif item.status == 'pending' %}is-warning
{% elif item.status == 'approved' %}is-info
{% else %}is-light{% endif %}">
{{ item.get_status_display }}
</span>
{% if item.last_error %}
<div class="has-text-danger csmsx-meta" title="{{ item.last_error }}">
خطا: {{ item.last_error|truncatechars:80 }}
</div>
{% endif %}
</td>
<td data-label="تحویل">
<span class="tag csmsx-badge
{% if item.delivery_status == 'delivered' %}is-success
{% elif item.delivery_status == 'not_delivered' %}is-danger
{% elif item.delivery_status == 'expired' %}is-danger
{% elif item.delivery_status == 'pending' %}is-warning
{% else %}is-light{% endif %}">
{{ item.get_delivery_status_display|default:'بدون پیگیری' }}
</span>
{% if item.delivery_checked_at %}
<div class="csmsx-meta csmsx-col-delivery-check">استعلام: {{ item.delivery_checked_at|toJalaliDT }}</div>
{% endif %}
</td>
<td class="csmsx-meta" data-label="تاریخ">{{ item.created_at|toJalaliDT }}</td>
<td class="csmsx-meta csmsx-col-sent" data-label="ارسال‌شده">{{ item.sent_at|toJalaliDT|default:'—' }}</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="has-text-centered py-5" data-label="">پیامکی برای این مشتری ثبت نشده است.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if items.has_other_pages %}
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
<nav class="pagination is-centered mt-4" role="navigation" aria-label="pagination">
{% if items.has_previous %}
<a class="pagination-previous" href="?page={{ items.previous_page_number }}{% if status %}&status={{ status }}{% endif %}{% if delivery %}&delivery={{ delivery }}{% endif %}">قبلی</a>
{% else %}
@@ -135,3 +353,235 @@
</div>
{% endblock %}
{% block BodyCustomer2 %}
{% if perms.admin_panel.ManageSMS %}
{# ---------- مودال ۱: تنظیمات پیامک (نوع‌ها + شماره‌های اصلی) ---------- #}
<div class="modal csmsx-modal" id="smsSettingsModal" role="dialog" aria-modal="true"
aria-labelledby="smsSettingsTitle">
<div class="modal-background js-csmsx-close"></div>
<div class="modal-content">
<div class="box" dir="rtl">
<div class="csmsx-modal__head">
<div>
<h2 class="csmsx-modal__title" id="smsSettingsTitle">⚙️ تنظیمات پیامک این مشتری</h2>
<p class="csmsx-modal__sub">
هر تغییر بلافاصله ذخیره می‌شود. خاموش‌کردن یک نوع، فقط برای همین مشتری است؛
متن پیامک‌ها از <b>قالب‌های پیامک</b> می‌آید.
</p>
</div>
<button type="button" class="delete js-csmsx-close" aria-label="بستن"></button>
</div>
<div class="csmsx-modal__body">
{% if notice_text and request.GET.open == 'settings' %}
<div class="notification {{ notice_kind }} is-light py-2 px-3 mb-3">
<strong>{{ notice_text }}</strong>
</div>
{% endif %}
{# --- بخش الف: شماره‌های اصلی پیامک --- #}
<section class="csmsx-section">
<h3 class="csmsx-section-title">📱 شماره‌های اصلی پیامک</h3>
{% if phone_entries %}
<div class="csmsx-phones">
{% for entry in phone_entries %}
{% if entry.is_mobile %}
<span class="csmsx-phone">
<span class="csmsx-phone__num">{{ entry.display }}</span>
<button type="button"
class="button sms-primary-toggle {% if entry.is_primary %}is-warning{% else %}is-light{% endif %}"
data-url="{% url 'admin_panel:sms_primary_toggle' customer.id %}"
data-phone="{{ entry.raw }}"
data-primary="{% if entry.is_primary %}true{% else %}false{% endif %}"
title="{% if entry.is_primary %}برداشتن از شماره‌های اصلی پیامک{% else %}تعیین به‌عنوان شمارهٔ اصلی پیامک{% endif %}">
{% if entry.is_primary %}★ اصلی{% else %}☆ اصلی{% endif %}
</button>
</span>
{% endif %}
{% endfor %}
</div>
<p class="csmsx-empty mt-2">
پیامک‌ها به شماره‌های «★ اصلی» می‌روند؛ اگر هیچ شماره‌ای اصلی نباشد، شمارهٔ اول مشتری.
</p>
{% else %}
<p class="csmsx-empty">شماره‌ای برای این مشتری ثبت نشده است.</p>
{% endif %}
</section>
{# --- بخش ب: فعال/غیرفعال کردن هر نوع پیامک --- #}
<section class="csmsx-section">
<h3 class="csmsx-section-title">
🔔 انواع پیامک
<span class="csmsx-tag">{{ sms_enabled_count }} از {{ sms_type_total }} فعال</span>
</h3>
{% for item in sms_toggles %}
<form method="post" action="{% url 'admin_panel:sms_customer_toggle' customer.id %}"
class="csmsx-type {% if not item.enabled %}is-off{% endif %}">
{% csrf_token %}
<input type="hidden" name="sms_type" value="{{ item.code }}">
<input type="hidden" name="is_enabled" value="{% if item.enabled %}false{% else %}true{% endif %}">
<span class="csmsx-type__icon" aria-hidden="true">{{ item.icon }}</span>
<div class="csmsx-type__main">
<span class="csmsx-type__label">{{ item.label }}</span>
<span class="csmsx-type__meta">
{% if item.when %}<span class="csmsx-tag">{{ item.when }}</span>{% endif %}
<span class="csmsx-tag {% if item.auto_send %}is-auto{% endif %}">
{% if item.auto_send %}ارسال خودکار{% else %}نیازمند تأیید مدیر{% endif %}
</span>
</span>
</div>
<button type="submit" class="csmsx-switch {% if item.enabled %}is-on{% endif %}"
role="switch" aria-checked="{% if item.enabled %}true{% else %}false{% endif %}"
title="{% if item.enabled %}خاموش کردن{% else %}روشن کردن{% endif %} این نوع پیامک">
<span class="csmsx-switch__track" aria-hidden="true"></span>
<span class="csmsx-switch__text">{% if item.enabled %}فعال{% else %}غیرفعال{% endif %}</span>
</button>
</form>
{% endfor %}
</section>
</div>
<div class="csmsx-modal__foot">
<button type="button" class="button is-light js-csmsx-close">بستن</button>
</div>
</div>
</div>
</div>
{# ---------- مودال ۲: ارسال پیامک سفارشی ---------- #}
<div class="modal csmsx-modal" id="smsCustomModal" role="dialog" aria-modal="true"
aria-labelledby="smsCustomTitle">
<div class="modal-background js-csmsx-close"></div>
<div class="modal-content">
<form class="box" dir="rtl" method="post"
action="{% url 'admin_panel:sms_send_custom' customer.id %}">
{% csrf_token %}
<div class="csmsx-modal__head">
<div>
<h2 class="csmsx-modal__title" id="smsCustomTitle">✉️ ارسال پیامک سفارشی</h2>
<p class="csmsx-modal__sub">
به شماره‌های «★ اصلی» این مشتری ارسال می‌شود و بی‌درنگ در صف قرار می‌گیرد.
</p>
</div>
<button type="button" class="delete js-csmsx-close" aria-label="بستن"></button>
</div>
<div class="csmsx-modal__body">
{% if notice_text and request.GET.open == 'custom' %}
<div class="notification {{ notice_kind }} is-light py-2 px-3 mb-3">
<strong>{{ notice_text }}</strong>
</div>
{% endif %}
<div class="csmsx-chips">
<button type="button" class="csmsx-chip js-csmsx-token" data-token="{name}">{name}</button>
<button type="button" class="csmsx-chip js-csmsx-token" data-token="{balance_sep}">{balance_sep}</button>
</div>
<textarea id="csmsxCustomText" class="textarea" name="text" rows="5"
placeholder="متن پیامک…" required
data-counter="#csmsxCustomCounter"></textarea>
<div class="csmsx-counter">
<span>{name} با نام مشتری، {balance_sep} با مانده حساب جایگزین می‌شود.</span>
<span id="csmsxCustomCounter">۰ نویسه</span>
</div>
</div>
<div class="csmsx-modal__foot">
<button type="button" class="button is-light js-csmsx-close">انصراف</button>
<button type="submit" class="button is-info">ارسال پیامک</button>
</div>
</form>
</div>
</div>
<script>
// باز/بسته کردن مودال‌های این صفحه + درج متغیر در متن + شمارندهٔ نویسه.
// بدون کتابخانهٔ جدید؛ کلید Esc و کلیک روی پس‌زمینه هم مودال را می‌بندد.
(function () {
if (window.__csmsxModalInit) { return; }
window.__csmsxModalInit = true;
function openModal(id) {
var modal = document.getElementById(id);
if (!modal) { return; }
modal.classList.add('is-active');
var field = modal.querySelector('textarea');
if (field) { field.focus(); }
}
function closeModal(modal) {
if (modal) { modal.classList.remove('is-active'); }
}
document.addEventListener('click', function (event) {
if (!event.target.closest) { return; }
var opener = event.target.closest('.js-csmsx-open');
if (opener) {
event.preventDefault();
openModal(opener.getAttribute('data-target'));
return;
}
var closer = event.target.closest('.js-csmsx-close');
if (closer) {
event.preventDefault();
closeModal(closer.closest('.modal'));
return;
}
var expand = event.target.closest('.js-csmsx-expand');
if (expand) {
event.preventDefault();
var box = expand.parentNode.querySelector('.csmsx-msg');
if (box) {
var opened = box.classList.toggle('is-clamped') === false;
expand.setAttribute('aria-expanded', opened ? 'true' : 'false');
expand.textContent = opened ? 'خلاصه' : 'نمایش کامل';
}
return;
}
var chip = event.target.closest('.js-csmsx-token');
if (chip) {
event.preventDefault();
var field = document.getElementById('csmsxCustomText');
if (!field) { return; }
var token = chip.getAttribute('data-token') || '';
var start = field.selectionStart == null ? field.value.length : field.selectionStart;
var end = field.selectionEnd == null ? field.value.length : field.selectionEnd;
field.value = field.value.slice(0, start) + token + field.value.slice(end);
field.selectionStart = field.selectionEnd = start + token.length;
field.dispatchEvent(new Event('input', { bubbles: true }));
field.focus();
}
});
document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape' && event.key !== 'Esc') { return; }
var active = document.querySelector('#smsSettingsModal.is-active, #smsCustomModal.is-active');
closeModal(active);
});
document.addEventListener('input', function (event) {
var field = event.target;
if (!field || field.id !== 'csmsxCustomText') { return; }
var target = document.querySelector(field.getAttribute('data-counter'));
if (!target) { return; }
// ارقام فارسی، هم‌خوان با بقیهٔ متن صفحه.
var count = String(field.value.length)
.replace(/[0-9]/g, function (d) { return '۰۱۲۳۴۵۶۷۸۹'.charAt(Number(d)); });
target.textContent = count + ' نویسه';
});
// باز شدن خودکار مودال بر اساس ?open= (بعد از ذخیرهٔ تنظیمات یا خطای ارسال).
var wanted = new URLSearchParams(window.location.search).get('open');
if (wanted === 'settings') { openModal('smsSettingsModal'); }
if (wanted === 'custom') { openModal('smsCustomModal'); }
})();
</script>
{% endif %}
{% endblock %}