some improve
This commit is contained in:
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 %}
|
||||
|
||||
Reference in new issue
Block a user