sms
This commit is contained in:
1 parent
250cdbab7f
commit
5c6069c3ef
87 files changed
+29209
-3697
No files matched your search
@@ -0,0 +1,70 @@
|
||||
{% 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-6-desktop is-12-mobile">
|
||||
<h3 class="subtitle is-6">شمارههای اصلی (گیرنده پیامک)</h3>
|
||||
{# همهٔ شمارهها از هر منبعی که ذخیره شدهاند نمایش داده میشوند (جدول شمارهها یا فیلدهای قدیمی) #}
|
||||
{% for entry in sms_phone_entries %}
|
||||
<form method="post" action="{% url 'admin_panel:sms_primary_toggle' customer.id %}" class="field has-addons">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="phone" value="{{ entry.raw }}">
|
||||
<input type="hidden" name="is_primary" value="{% if entry.is_primary %}false{% else %}true{% endif %}">
|
||||
<div class="control is-expanded">
|
||||
<input class="input" type="text"
|
||||
value="{{ entry.title }} — {{ entry.display }}{% if not entry.is_mobile %} (غیرقابل ارسال پیامک){% endif %}"
|
||||
disabled>
|
||||
</div>
|
||||
<div class="control">
|
||||
{% if entry.is_mobile %}
|
||||
<button class="button {% if entry.is_primary %}is-success{% else %}is-light{% endif %}" type="submit">
|
||||
{% if entry.is_primary %}اصلی ✓{% else %}تعیین بهعنوان اصلی{% endif %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="button is-light" type="button" disabled>غیرموبایل</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% empty %}
|
||||
<p class="help">شمارهای برای این مشتری ثبت نشده است.</p>
|
||||
{% endfor %}
|
||||
<p class="help">اگر شمارهٔ اصلی تعیین نشده باشد، شمارهٔ اولِ معتبر بهصورت خودکار استفاده میشود.</p>
|
||||
<p class="help">گیرندگان فعلی: {% for r in sms_recipients %}<span dir="ltr">{{ r }}</span>{% if not forloop.last %}، {% endif %}{% empty %}—{% endfor %}</p>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-desktop is-12-mobile">
|
||||
<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 %}
|
||||
@@ -19,6 +19,7 @@
|
||||
<script type="text/javascript" src="{% static 'admin_panel/persian-datepicker.min.js' %}"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'admin_panel/style.css' %}">
|
||||
{% block cssEnd %}{% endblock %}
|
||||
<style>
|
||||
</style>
|
||||
</head>
|
||||
@@ -119,9 +120,26 @@
|
||||
|
||||
|
||||
|
||||
{% if perms.admin_panel.ManageSMS %}
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">پیامک</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a href="{% url 'admin_panel:sms_settings' %}" style="padding-right: 10px;" class="navbar-item">
|
||||
تنظیمات سامانه
|
||||
</a>
|
||||
<a href="{% url 'admin_panel:sms_templates' %}" style="padding-right: 10px;" class="navbar-item">
|
||||
قالبها
|
||||
</a>
|
||||
<a href="{% url 'admin_panel:sms_queue' %}" style="padding-right: 10px;" class="navbar-item">
|
||||
صف ارسال
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a class="navbar-item">
|
||||
کاربران
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end ml-0">
|
||||
@@ -201,6 +219,17 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if notice_text %}
|
||||
<div class="columns m-0">
|
||||
<div class="column is-12 px-2 pt-3 pb-0">
|
||||
<div class="notification {{ notice_kind }} is-light mb-2">
|
||||
<button class="delete close-notification" type="button"></button>
|
||||
<strong>{{ notice_text }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block bodyPage %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -45,86 +45,29 @@
|
||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
||||
<label class="label">شمارههای تماس</label>
|
||||
<div id="dynamic-phones-edit">
|
||||
{% if customer.phones.all %}
|
||||
{% for phone_obj in customer.phones.all %}
|
||||
<div class="field has-addons mt-2 ss02">
|
||||
<div class="control" style="width: 30%;">
|
||||
<input disabled value="{{ phone_obj.title }}" name="phone_titles" class="input" type="text" placeholder="عنوان">
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<input disabled value="0{{ phone_obj.phone_number }}" name="phones" class="input an_phone" type="text" placeholder="تلفن">
|
||||
</div>
|
||||
<div class="control">
|
||||
<a href="tel:0{{ phone_obj.phone_number }}" class="button is-success" title="تماس با {{ phone_obj.phone_number }}">
|
||||
📞
|
||||
</a>
|
||||
</div>
|
||||
{% if perms.admin_panel.ViewStore %}
|
||||
<div class="control">
|
||||
<button type="button" disabled class="button is-danger remove-phone-btn">-</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% else %}
|
||||
{% if customer.phone1 %}
|
||||
<div class="field has-addons ss02 mt-2">
|
||||
<div class="control" style="width: 30%;">
|
||||
<input disabled value="تلفن 1" name="phone_titles" class="input" type="text" placeholder="عنوان">
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<input disabled value="0{{ customer.phone1 }}" name="phones" class="input an_phone" type="text" placeholder="تلفن">
|
||||
</div>
|
||||
<div class="control">
|
||||
<a href="tel:0{{ customer.phone1 }}" class="button is-success" title="تماس">
|
||||
📞
|
||||
</a>
|
||||
</div>
|
||||
{% if perms.admin_panel.ViewStore %}
|
||||
<div class="control"><button type="button" disabled class="button is-danger remove-phone-btn">-</button></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if customer.phone2 %}
|
||||
<div class="field has-addons ss02 mt-2">
|
||||
<div class="control" style="width: 30%;">
|
||||
<input disabled value="تلفن 2" name="phone_titles" class="input" type="text">
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<input disabled value="0{{ customer.phone2 }}" name="phones" class="input an_phone" type="text">
|
||||
</div>
|
||||
<div class="control">
|
||||
<a href="tel:0{{ customer.phone2 }}" class="button is-success" title="تماس">
|
||||
📞
|
||||
</a>
|
||||
</div>
|
||||
{% if perms.admin_panel.ViewStore %}
|
||||
<div class="control"><button type="button" disabled class="button is-danger remove-phone-btn">-</button></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if customer.phone3 %}
|
||||
<div class="field has-addons ss02 mt-2">
|
||||
<div class="control" style="width: 30%;">
|
||||
<input disabled value="تلفن 3" name="phone_titles" class="input" type="text">
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<input disabled value="0{{ customer.phone3 }}" name="phones" class="input an_phone" type="text">
|
||||
</div>
|
||||
<div class="control">
|
||||
<a href="tel:0{{ customer.phone3 }}" class="button is-success" title="تماس">
|
||||
📞
|
||||
</a>
|
||||
</div>
|
||||
{% if perms.admin_panel.ViewStore %}
|
||||
<div class="control"><button type="button" disabled class="button is-danger remove-phone-btn">-</button></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{# همهٔ شمارهها از هر منبعی که ذخیره شدهاند (جدول شمارهها + فیلدهای قدیمی)، بدون تکرار #}
|
||||
{% for entry in sms_phone_entries %}
|
||||
<div class="field has-addons mt-2 ss02">
|
||||
<div class="control" style="width: 30%;">
|
||||
<input disabled value="{{ entry.title }}" name="phone_titles" class="input" type="text" placeholder="عنوان">
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<input disabled value="{{ entry.display }}" name="phones" class="input an_phone" type="text" placeholder="تلفن">
|
||||
</div>
|
||||
<div class="control">
|
||||
<a href="tel:{{ entry.display }}" class="button is-success" title="تماس با {{ entry.display }}">
|
||||
📞
|
||||
</a>
|
||||
</div>
|
||||
{% if perms.admin_panel.ViewStore %}
|
||||
<div class="control">
|
||||
<button type="button" disabled class="button is-danger remove-phone-btn">-</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="help mt-2">شمارهای برای این مشتری ثبت نشده است.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if perms.admin_panel.ViewStore %}
|
||||
<button type="button" disabled id="add-more-phone-edit" class="button is-small is-info mt-2 w-100 add-phone-btn">
|
||||
@@ -148,6 +91,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
||||
<label class="label">توضیحات</label>
|
||||
<div class="control">
|
||||
<textarea disabled name="notes" class="textarea ss02 an_notes" rows="4" placeholder="یادداشت آزاد">{{ customer.notes }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
||||
<label class="label">لغو بازدید</label>
|
||||
<div class="control">
|
||||
@@ -686,7 +636,7 @@ $(document).ready(function() {
|
||||
var test = 0;
|
||||
|
||||
$("#enEdit").click(function(){
|
||||
$("#formcsEdit").find("input").prop('disabled', false);
|
||||
$("#formcsEdit").find("input, textarea").prop('disabled', false);
|
||||
// فعال کردن دکمههای کنترل تلفن
|
||||
$("#formcsEdit").find("button.remove-phone-btn, button.add-phone-btn").prop('disabled', false);
|
||||
|
||||
|
||||
@@ -180,9 +180,11 @@
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
{% include "./_customer_sms.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block BodyCustomer2 %}
|
||||
<div id="addNewIteam" class="modal">
|
||||
|
||||
@@ -136,63 +136,87 @@
|
||||
<button class="modal-close is-large closeSearch" aria-label="close"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal addNewUser">
|
||||
<div class="modal addNewUser" role="dialog" aria-modal="true" aria-labelledby="addUserTitle">
|
||||
<div class="modal-background closeAddUser"></div>
|
||||
<div class="modal-content">
|
||||
<form action="{% url 'admin_panel:customersAdd' %}" class="columns is-mobile is-multiline" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="field column is-4-desktop is-12-mobile m-0">
|
||||
<label class="label">نام ونام خانوادگی</label>
|
||||
<div class="control">
|
||||
<input required name="an_name" class="input an_name" type="text" placeholder="نام نام خانوادگی">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-8-desktop is-12-mobile m-0">
|
||||
<label class="label">آدرس</label>
|
||||
<div class="control">
|
||||
<input name="an_address" class="input an_address" type="text" placeholder="آدرس کامل">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-6-desktop is-12-mobile m-0">
|
||||
<label class="label">شمارههای تماس</label>
|
||||
<div id="dynamic-phones-add">
|
||||
<div class="field has-addons">
|
||||
<div class="control" style="width: 35%;">
|
||||
<input name="phone_titles" class="input" type="text" placeholder="عنوان (مثلاً موبایل)">
|
||||
<div class="modal-content add-modal">
|
||||
<div class="add-modal-card">
|
||||
<header class="add-modal-head">
|
||||
<h2 class="title is-5 mb-0" id="addUserTitle">ایجاد مشتری جدید</h2>
|
||||
<button type="button" class="delete closeAddUser" aria-label="بستن"></button>
|
||||
</header>
|
||||
|
||||
<form action="{% url 'admin_panel:customersAdd' %}" method="post" id="addCustomerForm">
|
||||
{% csrf_token %}
|
||||
<div class="add-modal-body">
|
||||
<div class="columns is-mobile is-multiline m-0">
|
||||
|
||||
<div class="field column is-8-desktop is-12-mobile py-2 px-2">
|
||||
<label class="label" for="addName">نام و نام خانوادگی <span class="has-text-danger">*</span></label>
|
||||
<div class="control">
|
||||
<input required name="an_name" id="addName" class="input an_name" type="text"
|
||||
autocomplete="name" placeholder="نام و نام خانوادگی">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<input name="phones" class="input an_phone" type="text" placeholder="شماره تماس">
|
||||
|
||||
<div class="field column is-4-desktop is-12-mobile py-2 px-2">
|
||||
<label class="label" for="addIdnCode">کدشناسایی</label>
|
||||
<div class="control">
|
||||
<input name="idnCode" id="addIdnCode" class="input idnCode" type="text"
|
||||
placeholder="کد شناسایی مشتری">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" class="button is-info add-phone-btn">+</button>
|
||||
|
||||
<div class="field column is-4-desktop is-12-mobile py-2 px-2">
|
||||
<label class="label" for="persianDateP">تاریخ نصب</label>
|
||||
<div class="control">
|
||||
<input name="installDate" id="persianDateP" class="input installDate" type="text"
|
||||
autocomplete="off" placeholder="مثل: 1400/06/01">
|
||||
</div>
|
||||
<p class="help">مبنای محاسبهٔ یادآوری بازدید دورهای.</p>
|
||||
</div>
|
||||
|
||||
<div class="field column is-8-desktop is-12-mobile py-2 px-2">
|
||||
<label class="label" for="addAddress">آدرس</label>
|
||||
<div class="control">
|
||||
<input name="an_address" id="addAddress" class="input an_address" type="text"
|
||||
autocomplete="street-address" placeholder="آدرس کامل">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-12 py-2 px-2">
|
||||
<label class="label">شمارههای تماس</label>
|
||||
<div id="dynamic-phones-add">
|
||||
<div class="phone-row">
|
||||
<div class="phone-row-fields">
|
||||
<input name="phone_titles" class="input phone-title" type="text"
|
||||
aria-label="عنوان شماره" placeholder="عنوان (مثلاً موبایل)">
|
||||
<input name="phones" class="input phone-number an_phone" type="tel"
|
||||
inputmode="tel" autocomplete="tel" aria-label="شماره تماس"
|
||||
placeholder="شماره تماس">
|
||||
</div>
|
||||
<div class="phone-row-actions">
|
||||
<label class="checkbox phone-primary" title="گیرندهٔ پیامکهای این مشتری">
|
||||
<input type="checkbox" name="phone_primary" value="0" checked> اصلی
|
||||
</label>
|
||||
<button type="button" class="button is-small is-danger is-light remove-phone-btn"
|
||||
aria-label="حذف این شماره">حذف</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="button is-info is-light is-small add-phone-btn"
|
||||
data-target="#dynamic-phones-add">+ افزودن شماره</button>
|
||||
<p class="help">تیک «اصلی» یعنی گیرندهٔ پیامکهای این مشتری (میتواند چند شماره باشد).</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-4-desktop is-12-mobile m-0">
|
||||
<label class="label">تاریخ نصب</label>
|
||||
<div class="control">
|
||||
<input name="installDate" id="persianDateP" class="input installDate" type="text" placeholder="مثل: 1400/06/01">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-4-desktop is-12-mobile m-0">
|
||||
<label class="label">کدشناسایی</label>
|
||||
<div class="control">
|
||||
<input name="idnCode" class="input idnCode" type="text" placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
||||
<input class="button is-success" id="DcreateCS" value="ایجاد مشتری" type="submit">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
<footer class="add-modal-foot">
|
||||
<button class="button is-success" id="DcreateCS" type="submit">ایجاد مشتری</button>
|
||||
<button class="button is-light closeAddUser" type="button">انصراف</button>
|
||||
</footer>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large closeAddUser" aria-label="close"></button>
|
||||
</div>
|
||||
@@ -252,83 +276,168 @@
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block cssEnd %}
|
||||
<style>
|
||||
/* --- مودال ایجاد مشتری: کارت با سربرگ/پابرگ چسبان و بدنهٔ اسکرولشو --- */
|
||||
/* .add-modal همان .modal-content بولماست؛ سقف ارتفاع و max-width پیشفرضش را خنثی
|
||||
میکنیم تا کارت خودش اندازه را تعیین کند (وگرنه پابرگ بریده میشود). */
|
||||
.add-modal {
|
||||
overflow: visible; max-height: none; max-width: 760px;
|
||||
width: 100%; margin: 0 auto; padding: 0 .5rem;
|
||||
}
|
||||
.add-modal-card {
|
||||
display: flex; flex-direction: column;
|
||||
background: #fff; border-radius: 10px; overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, .25);
|
||||
width: 100%; max-width: 720px; margin: 0 auto;
|
||||
max-height: calc(100vh - 120px);
|
||||
}
|
||||
/* فرم باید خودش ستون فلکس باشد تا بدنه بتواند اسکرول شود و پابرگ پایین بماند. */
|
||||
.add-modal-card > form {
|
||||
display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto;
|
||||
}
|
||||
.add-modal-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: .5rem; padding: .75rem 1rem;
|
||||
border-bottom: 1px solid #eee; background: #fff; flex: 0 0 auto;
|
||||
}
|
||||
.add-modal-body {
|
||||
overflow-y: auto; -webkit-overflow-scrolling: touch;
|
||||
padding: .25rem .5rem; flex: 1 1 auto; min-height: 0;
|
||||
}
|
||||
.add-modal-foot {
|
||||
display: flex; gap: .5rem; padding: .75rem 1rem;
|
||||
border-top: 1px solid #eee; background: #fafafa; flex: 0 0 auto;
|
||||
}
|
||||
.add-modal-foot .button { min-height: 2.6em; }
|
||||
|
||||
/* --- ردیف شمارهٔ تماس: دسکتاپ یکخطی، موبایل عمودی --- */
|
||||
.phone-row {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
|
||||
padding: .5rem; margin-bottom: .5rem;
|
||||
border: 1px solid #e6e6e6; border-radius: 8px; background: #fafafa;
|
||||
}
|
||||
.phone-row-fields { display: flex; flex-wrap: wrap; gap: .5rem; flex: 1 1 320px; min-width: 0; }
|
||||
.phone-row-fields .phone-title { flex: 0 0 140px; min-width: 0; }
|
||||
.phone-row-fields .phone-number { flex: 1 1 180px; min-width: 0; }
|
||||
.phone-row-actions { display: flex; align-items: center; gap: .75rem; white-space: nowrap; }
|
||||
.phone-primary { display: flex; align-items: center; gap: .35rem; font-size: .9rem; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.add-modal-card { max-height: calc(100vh - 32px); border-radius: 8px; }
|
||||
/* dvh روی مرورگرهای موبایل نوار آدرس را هم حساب میکند. */
|
||||
.add-modal-card { max-height: calc(100dvh - 32px); }
|
||||
.phone-row-fields { flex: 1 1 100%; }
|
||||
.phone-row-fields .phone-title,
|
||||
.phone-row-fields .phone-number { flex: 1 1 100%; }
|
||||
.phone-row-actions { flex: 1 1 100%; justify-content: space-between; }
|
||||
.add-modal-foot { flex-direction: column-reverse; }
|
||||
.add-modal-foot .button { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block jsEnd %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
|
||||
kamaDatepicker('persianDateP', { buttonsColor: "red", forceFarsiDigits: true });
|
||||
|
||||
$(document).on('click', '.add-phone-btn', function() {
|
||||
var phoneRow = `
|
||||
<div class="field has-addons mt-2">
|
||||
<div class="control" style="width: 35%;">
|
||||
<input name="phone_titles" class="input" type="text" placeholder="عنوان">
|
||||
var PHONE_ROW = `
|
||||
<div class="phone-row">
|
||||
<div class="phone-row-fields">
|
||||
<input name="phone_titles" class="input phone-title" type="text"
|
||||
aria-label="عنوان شماره" placeholder="عنوان (مثلاً موبایل)">
|
||||
<input name="phones" class="input phone-number an_phone" type="tel"
|
||||
inputmode="tel" autocomplete="tel" aria-label="شماره تماس" placeholder="شماره تماس">
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<input name="phones" class="input an_phone" type="text" placeholder="شماره تماس">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" class="button is-danger remove-phone-btn">-</button>
|
||||
<div class="phone-row-actions">
|
||||
<label class="checkbox phone-primary" title="گیرندهٔ پیامکهای این مشتری">
|
||||
<input type="checkbox" name="phone_primary" value="0"> اصلی
|
||||
</label>
|
||||
<button type="button" class="button is-small is-danger is-light remove-phone-btn"
|
||||
aria-label="حذف این شماره">حذف</button>
|
||||
</div>
|
||||
</div>`;
|
||||
$(this).closest('div[id^="dynamic-phones"]').append(phoneRow);
|
||||
|
||||
function reindexPhones() {
|
||||
$('#dynamic-phones-add .phone-row').each(function(i) {
|
||||
$(this).find('input[name="phone_primary"]').val(i);
|
||||
});
|
||||
}
|
||||
|
||||
// همیشه دستکم یک شماره «اصلی» بماند (شمارهٔ اول پیشفرضِ گیرندهٔ پیامک است).
|
||||
function ensurePrimary() {
|
||||
var $rows = $('#dynamic-phones-add .phone-row');
|
||||
if ($rows.length && $rows.find('input[name="phone_primary"]:checked').length === 0) {
|
||||
$rows.first().find('input[name="phone_primary"]').prop('checked', true);
|
||||
}
|
||||
}
|
||||
|
||||
reindexPhones();
|
||||
|
||||
$(document).on('click', '.add-phone-btn', function() {
|
||||
// data-target صریح (وقتی دکمه بیرونِ ظرف است)؛ وگرنه نزدیکترین ظرف شمارهها.
|
||||
var $target = $(this).data('target')
|
||||
? $($(this).data('target'))
|
||||
: $(this).closest('div[id^="dynamic-phones"]');
|
||||
$target.append(PHONE_ROW);
|
||||
reindexPhones();
|
||||
$target.find('.phone-row').last().find('.phone-number').focus();
|
||||
});
|
||||
|
||||
$(document).on('click', '.remove-phone-btn', function() {
|
||||
$(this).closest('.field.has-addons').remove();
|
||||
$(this).closest('.phone-row').remove();
|
||||
reindexPhones();
|
||||
ensurePrimary();
|
||||
});
|
||||
|
||||
$("#createCS").click(function(){
|
||||
var u_name = $(".addNewUser").find('.an_name').val();
|
||||
var u_address = $(".addNewUser").find('.an_address').val();
|
||||
var u_phone1 = $(".addNewUser").find('#phone-1').val();
|
||||
var u_phone2 = $(".addNewUser").find('#phone-2').val();
|
||||
var u_phone3 = $(".addNewUser").find('#phone-3').val();
|
||||
var u_installDate = $(".addNewUser").find('.installDate').val();
|
||||
alert("Ok send To server");
|
||||
function resetAddForm() {
|
||||
$('#addCustomerForm').find('input.an_name, input.an_address, input.installDate, input.idnCode').val('');
|
||||
$('#dynamic-phones-add').empty().append(PHONE_ROW);
|
||||
reindexPhones();
|
||||
ensurePrimary();
|
||||
}
|
||||
|
||||
$(".closeAddUser").click(function(){
|
||||
$(".addNewUser").removeClass("is-active");
|
||||
|
||||
$(".addNewUser").find('.an_name').val('');
|
||||
$(".addNewUser").find('.an_address').val('');
|
||||
$(".addNewUser").find('#phone-1').val('');
|
||||
$(".addNewUser").find('#phone-2').val('');
|
||||
$(".addNewUser").find('#phone-3').val('');
|
||||
$(".addNewUser").find('.installDate').val('');
|
||||
//Refresh page
|
||||
|
||||
});
|
||||
$(".modal-close, .modal-background").click(function(){
|
||||
$(".mEdit1").removeClass("is-active")
|
||||
});
|
||||
$(".closeSearch").click(function(){
|
||||
resetAddForm();
|
||||
});
|
||||
$(".modal-close, .modal-background").click(function(){
|
||||
$(".mEdit1").removeClass("is-active")
|
||||
});
|
||||
$(".closeSearch").click(function(){
|
||||
$(".SearchBox").removeClass("is-active")
|
||||
});
|
||||
$(".closeAddUser").click(function(){
|
||||
$(".addNewUser").removeClass("is-active");
|
||||
$(".addNewUser").find('.an_name').val('');
|
||||
$(".addNewUser").find('.an_address').val('');
|
||||
$(".addNewUser").find('#phone-1').val('');
|
||||
$(".addNewUser").find('#phone-2').val('');
|
||||
$(".addNewUser").find('#phone-3').val('');
|
||||
$(".addNewUser").find('.installDate').val('');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
$("#addNewUser").click(function(){
|
||||
$(".addNewUser").addClass("is-active")
|
||||
});
|
||||
$("#openSearchBox").click(function(){
|
||||
$("#addCustomerForm").on('submit', function(){
|
||||
// جلوگیری از ارسال دوباره؛ خودِ فرم همین حالا دادهها را بسته است.
|
||||
$(this).find('#DcreateCS').prop('disabled', true).text('در حال ثبت…');
|
||||
});
|
||||
|
||||
$("#addNewUser").click(function(){
|
||||
resetAddForm();
|
||||
$(".addNewUser").addClass("is-active");
|
||||
$('#addName').trigger('focus');
|
||||
});
|
||||
|
||||
$(document).on('keydown', function(e){
|
||||
if (e.key === 'Escape' && $(".addNewUser").hasClass("is-active")) {
|
||||
$(".addNewUser").removeClass("is-active");
|
||||
resetAddForm();
|
||||
}
|
||||
});
|
||||
|
||||
$("#openSearchBox").click(function(){
|
||||
$(".SearchBox").addClass("is-active")
|
||||
});
|
||||
$("#bedehiCN01").click(function(){
|
||||
});
|
||||
$("#bedehiCN01").click(function(){
|
||||
var factorId = $(this).parent().parent().find(".fid").val();
|
||||
if ($("#bedehiStatus").val() == "tasfiye"){
|
||||
alert("send To server to tasfiye");
|
||||
$("tbody").find('.fidv-'+factorId).parent().parent().remove();
|
||||
$(".mEdit1").removeClass("is-active");
|
||||
}
|
||||
});
|
||||
});
|
||||
} );
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,149 @@
|
||||
{% extends "./base.html" %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block bodyPage %}
|
||||
<div class="container mt-5" dir="rtl">
|
||||
<h1 class="title is-4">صف پیامکها</h1>
|
||||
|
||||
<div class="notification is-warning is-light">
|
||||
پیامکها تا زمانی که مدیر آنها را تأیید نکند ارسال نمیشوند. صف را بازبینی
|
||||
کنید، موارد نامناسب را حذف کنید و سپس تأیید بزنید.
|
||||
<br>
|
||||
حذف یک مورد، فقط همان نوبت را کنسل میکند. یادآوریهای دورهای (بازدید و ثبت
|
||||
اطلاعات چک) در <strong>دورهٔ بعدی</strong> دوباره ساخته میشوند؛ پس با حذف یک
|
||||
مورد، پیامک برای آن مشتری برای همیشه قطع نمیشود.
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline mb-2" dir="rtl">
|
||||
<div class="column is-narrow">
|
||||
<div class="box py-3 px-4 has-text-centered">
|
||||
<p class="heading mb-0">کل صف</p>
|
||||
<p class="title is-5 mb-0">{{ total_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% for code, label, count in status_stats %}
|
||||
<div class="column is-narrow">
|
||||
<a href="?status={{ code }}" class="box py-3 px-4 has-text-centered"
|
||||
{% if status == code %}style="border:2px solid #3273dc"{% endif %}>
|
||||
<p class="heading mb-0">{{ label }}</p>
|
||||
<p class="title is-5 mb-0">{{ count }}</p>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if status or delivery %}
|
||||
<p class="mb-3">نتایج فیلترشده: <strong>{{ filtered_count }}</strong> مورد — <a href="{% url 'admin_panel:sms_queue' %}">حذف فیلتر</a></p>
|
||||
{% endif %}
|
||||
|
||||
<form method="get" class="field has-addons mb-3">
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select name="status">
|
||||
<option value="">همهٔ وضعیتها</option>
|
||||
{% for value, label in status_choices %}
|
||||
<option value="{{ value }}" {% if status == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select 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" type="submit">فیلتر</button></div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="{% url 'admin_panel:sms_queue_bulk' %}" id="bulk-form" class="mb-3">
|
||||
{% csrf_token %}
|
||||
<button class="button is-success" name="action" value="approve_all"
|
||||
onclick="return confirm('همهٔ پیامکهای «در انتظار تأیید» تأیید و ارسال شوند؟');">
|
||||
تأیید همهٔ موارد در انتظار ({{ pending_count }})
|
||||
</button>
|
||||
<button class="button is-info" name="action" value="approve_selected">تأیید انتخابشدهها</button>
|
||||
<button class="button is-danger" name="action" value="delete_selected"
|
||||
onclick="return confirm('موارد انتخابشده از صف حذف شوند؟');">حذف انتخابشدهها</button>
|
||||
</form>
|
||||
|
||||
<div class="notification is-danger is-light is-flex is-align-items-center is-justify-content-space-between mb-3">
|
||||
<span>خالی کردن کل صف: همهٔ پیامکها (در هر وضعیتی) حذف میشوند و دیگر بازنمیگردند.</span>
|
||||
<form method="post" action="{% url 'admin_panel:sms_queue_empty' %}"
|
||||
onsubmit="return confirm('کل صف پیامک خالی شود؟ این پیامکها دیگر هرگز ارسال یا بازسازی نمیشوند.');">
|
||||
{% csrf_token %}
|
||||
<button class="button is-danger" type="submit">خالی کردن کل صف</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-narrow is-hoverable is-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" onclick="document.querySelectorAll('.row-check').forEach(c => c.checked = this.checked)"></th>
|
||||
<th>شناسه</th><th>مشتری</th><th>نوع</th><th>شماره</th>
|
||||
<th>وضعیت</th><th>دلیوری</th><th>تلاش</th><th>ایجاد</th><th>ارسال</th><th>خطا</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in queue %}
|
||||
<tr>
|
||||
<td><input class="row-check" type="checkbox" form="bulk-form" name="ids" value="{{ item.id }}"></td>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>
|
||||
{% if item.customer_id %}
|
||||
<a href="{% url 'admin_panel:customer' item.customer_id %}">{{ item.customer.name_family }}</a>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
<td>{{ item.get_sms_type_display }}</td>
|
||||
<td dir="ltr">{{ item.phone_number }}</td>
|
||||
<td>{{ item.get_status_display }}</td>
|
||||
<td>{{ item.get_delivery_status_display|default:'—' }}</td>
|
||||
<td>{{ item.attempts }}</td>
|
||||
<td>{{ item.created_at|toJalaliDT }}</td>
|
||||
<td>{{ item.sent_at|toJalaliDT|default:'—' }}</td>
|
||||
<td class="is-size-7">{{ item.last_error|truncatechars:60 }}</td>
|
||||
<td>
|
||||
<div class="buttons are-small">
|
||||
{% if item.status == 'pending' %}
|
||||
<form method="post" action="{% url 'admin_panel:sms_queue_approve' item.id %}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-small is-success" type="submit">تأیید</button>
|
||||
</form>
|
||||
{% elif item.status == 'failed' %}
|
||||
<form method="post" action="{% url 'admin_panel:sms_queue_retry' item.id %}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-small is-warning" type="submit">ارسال مجدد</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="{% url 'admin_panel:sms_queue_delete' item.id %}"
|
||||
onclick="return confirm('این پیامک از صف حذف شود؟');">
|
||||
{% csrf_token %}
|
||||
<button class="button is-small is-danger" type="submit">حذف</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="12" class="has-text-centered">موردی یافت نشد.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if queue.has_other_pages %}
|
||||
<nav class="pagination" role="navigation" aria-label="pagination">
|
||||
{% if queue.has_previous %}
|
||||
<a class="pagination-previous" href="?page={{ queue.previous_page_number }}{% if status %}&status={{ status }}{% endif %}{% if delivery %}&delivery={{ delivery }}{% endif %}">قبلی</a>
|
||||
{% endif %}
|
||||
{% if queue.has_next %}
|
||||
<a class="pagination-next" href="?page={{ queue.next_page_number }}{% if status %}&status={{ status }}{% endif %}{% if delivery %}&delivery={{ delivery }}{% endif %}">بعدی</a>
|
||||
{% endif %}
|
||||
<ul class="pagination-list"><li><span class="pagination-link">صفحه {{ queue.number }} از {{ queue.paginator.num_pages }}</span></li></ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,188 @@
|
||||
{% extends "./base.html" %}
|
||||
|
||||
{% block bodyPage %}
|
||||
<div class="container mt-5" dir="rtl">
|
||||
<h1 class="title is-4">تنظیمات سامانه پیامک</h1>
|
||||
|
||||
<div class="box">
|
||||
<form method="post" action="{% url 'admin_panel:sms_settings' %}">
|
||||
{% csrf_token %}
|
||||
<div class="columns is-multiline">
|
||||
<div class="field column is-4-desktop is-12-mobile">
|
||||
<label class="label">سرویسدهنده</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="provider">
|
||||
{% for value, label in provider_choices %}
|
||||
<option value="{{ value }}" {% if config.provider == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-4-desktop is-12-mobile">
|
||||
<label class="label">وضعیت ارسال</label>
|
||||
<div class="control">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="is_enabled" {% if config.is_enabled %}checked{% endif %}>
|
||||
فعال باشد
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-4-desktop is-12-mobile">
|
||||
<label class="label">مهلت ارتباط (ثانیه)</label>
|
||||
<div class="control">
|
||||
<input class="input" type="number" name="timeout_seconds" value="{{ config.timeout_seconds }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-12-mobile">
|
||||
<label class="label">آدرس سرویس پیامک (URL)</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="api_url" value="{{ config.api_url }}" dir="ltr">
|
||||
</div>
|
||||
<p class="help">
|
||||
برای 0098SMS همین آدرس لینکی ارسال است
|
||||
(<span dir="ltr">http://www.0098sms.com/sendsmslink.aspx</span>)؛ این سرویس
|
||||
WSDL/SOAP ندارد و آدرس WSDL باعث خطای «Invalid XML content» میشود.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field column is-12-mobile">
|
||||
<label class="label">آدرس وبسرویس (SOAP/WSDL) — فقط provider وبسرویس</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="webservice_url" value="{{ config.webservice_url }}" dir="ltr">
|
||||
</div>
|
||||
<p class="help">
|
||||
ارسال با شناسهٔ پیامک (<span dir="ltr">smsid</span>) و استعلام تحویل به گوشی
|
||||
از همین آدرس انجام میشود. پیشفرض:
|
||||
<span dir="ltr">https://webservice.0098sms.com/service.asmx</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field column is-4-desktop is-12-mobile">
|
||||
<label class="label">شمارهٔ پنل (PnlNo)</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="panel_no" value="{{ config.panel_no }}" dir="ltr">
|
||||
</div>
|
||||
<p class="help">اگر خالی بماند از «شماره خط فرستنده» استفاده میشود.</p>
|
||||
</div>
|
||||
|
||||
<div class="field column is-4-desktop is-12-mobile">
|
||||
<label class="label">نام کاربری</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="username" value="{{ config.username }}" dir="ltr">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-4-desktop is-12-mobile">
|
||||
<label class="label">رمز عبور</label>
|
||||
<div class="control">
|
||||
<input class="input" type="password" name="password" placeholder="{% if config.password %}(بدون تغییر){% endif %}" dir="ltr">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-4-desktop is-12-mobile">
|
||||
<label class="label">شماره خط فرستنده</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="sender" value="{{ config.sender }}" dir="ltr">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-12-mobile">
|
||||
<label class="label">توکن API (فقط برای provider HTTP)</label>
|
||||
<div class="control">
|
||||
<input class="input" type="password" name="api_token" placeholder="{% if config.api_token %}(بدون تغییر){% endif %}" dir="ltr">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h2 class="title is-5">کنترل جریان ارسال</h2>
|
||||
<p class="help mb-3">
|
||||
برای جلوگیری از سیل پیامک: تنها مواردی ارسال میشوند که پنجرهٔ یادآوریشان از
|
||||
«خط شروع» به بعد باز شود. موارد قدیمیتر تنها با روشنکردن «اجازهٔ موارد قدیمی»
|
||||
و در سقف تعیینشده ارسال میشوند. بین هر پیامک هم فاصلهٔ زمانی رعایت میشود.
|
||||
</p>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
<div class="field column is-6-desktop is-12-mobile">
|
||||
<label class="label">تاریخ شروع دوره</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="scan_since"
|
||||
value="{{ scan_since_jalali }}"
|
||||
placeholder="مثلاً 1403/07/18 — خالی = همین حالا" dir="ltr">
|
||||
</div>
|
||||
<p class="help">
|
||||
فقط یادآوریهایی صف میشوند که «پنجرهٔ» آنها از این تاریخ به بعد باز شده باشد.
|
||||
مشتریان قدیمیتری که پنجرهشان پیش از این تاریخ باز شده، وارد صف نمیشوند.
|
||||
قالب: <span dir="ltr">YYYY/MM/DD</span> (شمسی) — خالی = همین حالا.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field column is-3-desktop is-6-mobile">
|
||||
<label class="label">حداکثر پیامک هر اسکنر در هر اجرا</label>
|
||||
<div class="control">
|
||||
<input class="input" type="number" min="0" name="max_per_run" value="{{ config.max_per_run }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-3-desktop is-6-mobile">
|
||||
<label class="label">فاصلهٔ بین هر پیامک (ثانیه)</label>
|
||||
<div class="control">
|
||||
<input class="input" type="number" min="0" name="send_interval_seconds" value="{{ config.send_interval_seconds }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-3-desktop is-6-mobile">
|
||||
<label class="label">اجازهٔ ارسال موارد قدیمی</label>
|
||||
<div class="control">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="allow_backlog" {% if config.allow_backlog %}checked{% endif %}>
|
||||
فعال باشد
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-3-desktop is-6-mobile">
|
||||
<label class="label">حداکثر موارد قدیمی در هر اجرا</label>
|
||||
<div class="control">
|
||||
<input class="input" type="number" min="0" name="backlog_max" value="{{ config.backlog_max }}">
|
||||
</div>
|
||||
<p class="help">صفر = هیچ مورد قدیمی ارسال نمیشود.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button class="button is-success" type="submit">ذخیره تنظیمات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">ارسال پیامک آزمایشی</h2>
|
||||
<form method="post" action="{% url 'admin_panel:sms_settings' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="test">
|
||||
<div class="field">
|
||||
<label class="label">متن پیامک آزمایشی</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" name="test_message" rows="3">{{ config.test_message }}</textarea>
|
||||
</div>
|
||||
<p class="help">این متن ذخیره میشود و در ارسالهای بعدی هم استفاده خواهد شد.</p>
|
||||
</div>
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<input class="input" type="text" name="test_phone" placeholder="مثلاً 09120000000" dir="ltr">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-info" type="submit">ارسال آزمایشی</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<p class="help">پیامک آزمایشی در صف قرار میگیرد و توسط worker ارسال میشود.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,455 @@
|
||||
{% extends "./base.html" %}
|
||||
|
||||
{% block bodyPage %}
|
||||
<style>
|
||||
{% verbatim %}
|
||||
/* --- صفحهٔ قالبهای پیامک: استایلها با پیشوند smsx و محدود به همین صفحه --- */
|
||||
|
||||
/* هیچ عنصری از این صفحه نباید از کادر خودش بیرون بزند؛ ریشهٔ بههمریختگیها
|
||||
معمولاً همین box-sizing است (width:100% + padding). */
|
||||
.smsx, .smsx *, .smsx *::before, .smsx *::after { box-sizing: border-box; }
|
||||
.smsx { max-width: 60rem; margin-inline: auto; }
|
||||
.smsx img, .smsx textarea, .smsx input, .smsx button { max-width: 100%; }
|
||||
|
||||
.smsx-head { display: flex; flex-wrap: wrap; gap: .75rem; align-items: flex-start; justify-content: space-between; margin-bottom: 1rem; }
|
||||
.smsx-head__actions { display: flex; flex-wrap: wrap; gap: .5rem; }
|
||||
|
||||
/* ---------- کارت هر نوع پیامک ---------- */
|
||||
.smsx-card { background: #fff; border: 1px solid #e8e8e8; border-radius: 12px; box-shadow: 0 1px 2px rgba(0,0,0,.04); margin-bottom: .9rem; overflow: hidden; }
|
||||
.smsx-card[open] { box-shadow: 0 6px 18px rgba(0,0,0,.07); border-color: #d9d9d9; }
|
||||
.smsx-card.is-off .smsx-summary__title { color: #9a9a9a; }
|
||||
.smsx-card.is-dirty { border-color: #ffdd57; }
|
||||
|
||||
.smsx-summary { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; gap: .75rem; align-items: center; padding: .9rem 1rem; cursor: pointer; list-style: none; }
|
||||
.smsx-summary::-webkit-details-marker { display: none; }
|
||||
.smsx-summary:hover { background: #fafafa; }
|
||||
.smsx-summary:focus-visible { outline: 2px solid #3273dc; outline-offset: -2px; }
|
||||
.smsx-summary__icon { font-size: 1.35rem; line-height: 1; }
|
||||
.smsx-summary__main { min-width: 0; }
|
||||
.smsx-summary__title { display: block; font-weight: 600; color: #363636; overflow-wrap: anywhere; }
|
||||
.smsx-summary__sub { display: block; font-size: .78rem; color: #7a7a7a; line-height: 1.7; overflow-wrap: anywhere; }
|
||||
.smsx-summary__meta { display: flex; gap: .35rem; align-items: center; flex-wrap: wrap; }
|
||||
.smsx-summary__chev { flex: 0 0 auto; width: .55rem; height: .55rem; border-right: 2px solid #b5b5b5; border-bottom: 2px solid #b5b5b5; transform: rotate(45deg); transition: transform .18s; }
|
||||
.smsx-card[open] .smsx-summary__chev { transform: rotate(-135deg); }
|
||||
|
||||
/* ---------- بدنهٔ فرم ---------- */
|
||||
.smsx-body { padding: 1rem; border-top: 1px solid #f0f0f0; }
|
||||
/* گرید سهستونی مقاوم: ستون عنوان کشسان، دو ستون دیگر بهقدر محتوا */
|
||||
.smsx-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 10rem) max-content; gap: .9rem 1rem; align-items: start; }
|
||||
.smsx-cell { display: flex; flex-direction: column; gap: .4rem; min-width: 0; }
|
||||
.smsx-label { display: block; margin: 0; font-size: .8rem; font-weight: 600; color: #4a4a4a; line-height: 1.6; overflow-wrap: anywhere; }
|
||||
.smsx-cell--switch .smsx-label { white-space: nowrap; }
|
||||
.smsx-note { margin-top: .75rem; margin-bottom: 0; line-height: 1.9; }
|
||||
|
||||
/* کلید فعال/غیرفعال */
|
||||
.smsx-switch { display: inline-flex; align-items: center; gap: .5rem; cursor: pointer; user-select: none; min-height: 2.5rem; }
|
||||
.smsx-switch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
|
||||
.smsx-switch__track { position: relative; flex: 0 0 auto; width: 46px; height: 26px; border-radius: 13px; background: #dbdbdb; transition: background .18s; }
|
||||
.smsx-switch__track::after { content: ""; position: absolute; top: 3px; inset-inline-start: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3); transition: transform .18s; }
|
||||
.smsx-switch input:checked + .smsx-switch__track { background: #48c774; }
|
||||
.smsx-switch input:checked + .smsx-switch__track::after { transform: translateX(-20px); }
|
||||
.smsx-switch input:focus-visible + .smsx-switch__track { outline: 2px solid #3273dc; outline-offset: 2px; }
|
||||
.smsx-switch__text { font-size: .85rem; color: #4a4a4a; white-space: nowrap; }
|
||||
|
||||
/* ---------- کادر نوشتن متن ---------- */
|
||||
.smsx-editor { margin-top: 1rem; border: 1px solid #e2e2e2; border-radius: 10px; background: #fff; overflow: hidden; transition: border-color .15s, box-shadow .15s; }
|
||||
.smsx-editor:focus-within { border-color: #3273dc; box-shadow: 0 0 0 3px rgba(50,115,220,.12); }
|
||||
/* padding افقی سر/پا هماندازهٔ padding خود textarea (1rem) تا متن و برچسب همتراز بمانند */
|
||||
.smsx-editor__head { display: flex; flex-wrap: wrap; gap: .5rem .75rem; align-items: center; justify-content: space-between; padding: .65rem 1rem; background: #fbfbfb; border-bottom: 1px solid #eee; }
|
||||
.smsx-chips { display: flex; flex-wrap: wrap; gap: .35rem; }
|
||||
.smsx-chip { display: inline-block; font-size: .75rem; line-height: 1.6; padding: .35rem .65rem; border-radius: 999px; border: 1px solid #dbdbdb; background: #fff; color: #4a4a4a; cursor: pointer; direction: ltr; font-family: ui-monospace, Menlo, Consolas, monospace; white-space: nowrap; }
|
||||
.smsx-chip:hover { border-color: #3273dc; color: #3273dc; background: #f5f9ff; }
|
||||
.smsx-chip:active { transform: translateY(1px); }
|
||||
|
||||
/* textarea دقیقاً به اندازهٔ کادر؛ بدون border/padding سرکش */
|
||||
.smsx-editor textarea.smsx-textarea {
|
||||
display: block; width: 100%; max-width: 100%; margin: 0;
|
||||
border: 0; border-radius: 0; box-shadow: none; background: transparent;
|
||||
padding: .85rem 1rem; font-size: 1rem; line-height: 1.9; font-family: inherit;
|
||||
resize: vertical; overflow-y: auto;
|
||||
unicode-bidi: plaintext; text-align: start; overflow-wrap: anywhere;
|
||||
}
|
||||
.smsx-editor textarea.smsx-textarea:focus { border: 0; box-shadow: none; outline: none; }
|
||||
.smsx-editor__foot { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem .6rem; padding: .5rem 1rem; border-top: 1px solid #f0f0f0; background: #fbfbfb; font-size: .78rem; color: #7a7a7a; }
|
||||
.smsx-dot { color: #dbdbdb; }
|
||||
.smsx-metric { font-variant-numeric: tabular-nums; }
|
||||
.smsx-metric.is-ok { color: #257942; }
|
||||
.smsx-metric.is-warn { color: #b8860b; }
|
||||
.smsx-metric.is-bad { color: #cc0f35; font-weight: 600; }
|
||||
.smsx-warn { color: #cc0f35; overflow-wrap: anywhere; }
|
||||
|
||||
/* ---------- پیشنمایش ---------- */
|
||||
.smsx-preview { margin-top: 1rem; }
|
||||
.smsx-preview__label { display: block; font-size: .8rem; font-weight: 600; color: #4a4a4a; margin-bottom: .4rem; }
|
||||
.smsx-bubble { background: #ecf7ee; border: 1px solid #d5ecda; border-radius: 14px 14px 4px 14px; padding: .75rem .95rem; line-height: 1.9; font-size: .9rem; white-space: pre-wrap; overflow-wrap: anywhere; min-height: 2.6rem; }
|
||||
.smsx-preview__hint { margin: .4rem 0 0; font-size: .75rem; line-height: 1.8; }
|
||||
|
||||
.smsx-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; margin-top: 1rem; }
|
||||
.smsx-dirty { font-size: .8rem; color: #b8860b; }
|
||||
|
||||
.smsx-help { margin-bottom: 1rem; background: #f5f9ff; border: 1px solid #dbe9ff; border-radius: 10px; padding: .7rem .95rem; }
|
||||
.smsx-help > summary { cursor: pointer; font-size: .85rem; color: #3273dc; font-weight: 600; }
|
||||
.smsx-help .smsx-chips { margin-top: .6rem; }
|
||||
.smsx-help .smsx-chip { cursor: default; white-space: normal; }
|
||||
|
||||
/* ---------- موبایل ---------- */
|
||||
@media (max-width: 768px) {
|
||||
.smsx-body { padding: .85rem; }
|
||||
.smsx-summary { grid-template-columns: auto minmax(0, 1fr) auto; padding: .8rem .85rem; row-gap: .5rem; }
|
||||
.smsx-summary__meta { grid-column: 1 / -1; }
|
||||
/* همهٔ فیلدها زیر هم؛ دیگر هیچ چیز به هم نمیچسبد */
|
||||
.smsx-grid { grid-template-columns: minmax(0, 1fr); }
|
||||
.smsx-cell--switch .smsx-label { white-space: normal; }
|
||||
.smsx-editor textarea.smsx-textarea { font-size: 16px; } /* جلوگیری از زوم خودکار iOS */
|
||||
.smsx-actions .button { width: 100%; }
|
||||
}
|
||||
{% endverbatim %}
|
||||
</style>
|
||||
|
||||
<div class="container mt-5" dir="rtl">
|
||||
<div class="smsx">
|
||||
<div class="smsx-head">
|
||||
<div>
|
||||
<h1 class="title is-4 mb-1">قالبهای پیامک</h1>
|
||||
<p class="subtitle is-6 has-text-grey mb-0">
|
||||
متن هر نوع پیامک و زمان ارسال آن را اینجا تعیین کنید. برای ویرایش، روی هر کارت بزنید.
|
||||
</p>
|
||||
</div>
|
||||
<div class="smsx-head__actions">
|
||||
<button type="button" class="button is-small is-light" data-smsx-expand-all>باز کردن همه</button>
|
||||
<button type="button" class="button is-small is-light" data-smsx-collapse-all>بستن همه</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details class="smsx-help">
|
||||
<summary>متغیرهای قابل استفاده در متن پیامک</summary>
|
||||
<p class="help mb-0">کافی است روی هر متغیر بزنید تا در کادر متن همان پیامک درج شود.</p>
|
||||
<div class="smsx-chips">
|
||||
{% for token, desc in variables %}
|
||||
<span class="smsx-chip" title="{{ desc }}">{{ token }} <span class="has-text-grey">{{ desc }}</span></span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{{ smsx_config|json_script:"smsx-config" }}
|
||||
|
||||
{% for row in rows %}
|
||||
<details class="smsx-card" id="type-{{ row.code }}" {% if forloop.first %}open{% endif %} data-smsx-card>
|
||||
<summary class="smsx-summary">
|
||||
<span class="smsx-summary__icon" aria-hidden="true">{{ row.icon }}</span>
|
||||
<span class="smsx-summary__main">
|
||||
<span class="smsx-summary__title">{{ row.label }}</span>
|
||||
<span class="smsx-summary__sub">
|
||||
{{ row.when }}{% if row.timing_value is not None %} · {{ row.timing_label }}: {{ row.timing_value }}{% endif %}
|
||||
</span>
|
||||
</span>
|
||||
<span class="smsx-summary__meta">
|
||||
<span class="tag is-light" data-smsx-badge>—</span>
|
||||
{% if not row.template.is_enabled %}
|
||||
<span class="tag is-warning is-light">غیرفعال</span>
|
||||
{% endif %}
|
||||
{% if row.template.auto_send %}
|
||||
<span class="tag is-success is-light">خودکار</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="smsx-summary__chev" aria-hidden="true"></span>
|
||||
</summary>
|
||||
|
||||
<form class="smsx-body" method="post" action="{% url 'admin_panel:sms_template_edit' row.template.id %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="smsx-grid">
|
||||
<div class="smsx-cell">
|
||||
<label class="smsx-label" for="title-{{ row.code }}">عنوان</label>
|
||||
<input id="title-{{ row.code }}" class="input" type="text" name="title" value="{{ row.template.title }}">
|
||||
</div>
|
||||
|
||||
{% if row.timing_value is not None %}
|
||||
<div class="smsx-cell">
|
||||
<label class="smsx-label" for="timing-{{ row.code }}">{{ row.timing_label }}</label>
|
||||
<input id="timing-{{ row.code }}" class="input" type="number" inputmode="numeric" min="0"
|
||||
name="timing_value" value="{{ row.timing_value|default_if_none:'' }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="smsx-cell smsx-cell--switch">
|
||||
<span class="smsx-label">وضعیت ارسال</span>
|
||||
<label class="smsx-switch">
|
||||
<input type="checkbox" name="is_enabled" {% if row.template.is_enabled %}checked{% endif %}>
|
||||
<span class="smsx-switch__track"></span>
|
||||
<span class="smsx-switch__text">{% if row.template.is_enabled %}فعال{% else %}غیرفعال{% endif %}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="smsx-cell smsx-cell--switch">
|
||||
<span class="smsx-label">ارسال</span>
|
||||
<label class="smsx-switch" title="روشن = ارسال خودکار بدون صف تأیید مدیر / خاموش = نیازمند تأیید مدیر">
|
||||
<input type="checkbox" name="auto_send" {% if row.template.auto_send %}checked{% endif %}>
|
||||
<span class="smsx-switch__track"></span>
|
||||
<span class="smsx-switch__text">{% if row.template.auto_send %}خودکار{% else %}با تأیید مدیر{% endif %}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if row.timing.note %}
|
||||
<p class="help smsx-note">{{ row.timing.note }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="smsx-editor">
|
||||
<div class="smsx-editor__head">
|
||||
<label class="smsx-label" for="text-{{ row.code }}">متن پیامک</label>
|
||||
<div class="smsx-chips" role="group" aria-label="درج متغیر در متن">
|
||||
{% for token, desc in variables %}
|
||||
<button type="button" class="smsx-chip" data-smsx-insert="{{ token }}"
|
||||
title="{{ desc }}" aria-label="درج {{ token }} — {{ desc }}">{{ token }}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="text-{{ row.code }}" class="textarea smsx-textarea" name="text" rows="6"
|
||||
data-smsx-text aria-describedby="meta-{{ row.code }}"
|
||||
placeholder="متن پیامک را اینجا بنویسید…">{{ row.template.text }}</textarea>
|
||||
<div class="smsx-editor__foot" id="meta-{{ row.code }}">
|
||||
<span class="smsx-metric" data-smsx-counter>—</span>
|
||||
<span class="smsx-dot" aria-hidden="true">•</span>
|
||||
<span class="smsx-metric" data-smsx-segments>—</span>
|
||||
<span class="smsx-warn" data-smsx-warn hidden></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="smsx-preview">
|
||||
<span class="smsx-preview__label">پیشنمایش (با دادهٔ نمونه)</span>
|
||||
<div class="smsx-bubble" data-smsx-preview>{{ row.preview }}</div>
|
||||
<p class="help smsx-preview__hint">
|
||||
مقادیر نمونه فقط برای نمایش است؛ هنگام ارسال، اطلاعات واقعی همان مشتری جای متغیرها مینشیند.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="smsx-actions">
|
||||
<button class="button is-success" type="submit">ذخیره</button>
|
||||
<span class="smsx-dirty" data-smsx-dirty hidden>● تغییرات ذخیرهنشده</span>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
{% empty %}
|
||||
<div class="notification is-warning">هیچ قالبی تعریف نشده است.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block jsEnd %}
|
||||
<script>
|
||||
{% verbatim %}
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var configEl = document.getElementById("smsx-config");
|
||||
if (!configEl) return;
|
||||
var CFG = JSON.parse(configEl.textContent);
|
||||
var TOKENS = CFG.tokens || [];
|
||||
var SAMPLE = CFG.sample || {};
|
||||
|
||||
var TOKEN_RE = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
||||
// GSM-7 پایه (یک نویسه) و نویسههای افزودنی (دو نویسه)
|
||||
var GSM_BASIC = {};
|
||||
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n\r".split("").forEach(function (c) { GSM_BASIC[c] = true; });
|
||||
var GSM_EXT = {};
|
||||
["^", "{", "}", "[", "]", "~", "|", "\\", "€"].forEach(function (c) { GSM_EXT[c] = true; });
|
||||
|
||||
function toFa(value) {
|
||||
return String(value).replace(/\d/g, function (d) { return "۰۱۲۳۴۵۶۷۸۹"[d]; });
|
||||
}
|
||||
|
||||
// شمارش نویسه و بخشهای پیامک: GSM-7 ⇒ ۱۶۰/۱۵۳، یونیکد ⇒ ۷۰/۶۷
|
||||
function measure(text) {
|
||||
var chars = Array.from(text);
|
||||
var gsm = true, len = 0;
|
||||
for (var i = 0; i < chars.length; i++) {
|
||||
var c = chars[i];
|
||||
if (GSM_BASIC[c]) { len += 1; }
|
||||
else if (GSM_EXT[c]) { len += 2; }
|
||||
else { gsm = false; break; }
|
||||
}
|
||||
if (!gsm) len = chars.length;
|
||||
var single = gsm ? 160 : 70, multi = gsm ? 153 : 67;
|
||||
var parts = len === 0 ? 0 : (len <= single ? 1 : Math.ceil(len / multi));
|
||||
return { len: len, parts: parts, gsm: gsm };
|
||||
}
|
||||
|
||||
function fillSample(text) {
|
||||
return text.replace(TOKEN_RE, function (match, name) {
|
||||
return Object.prototype.hasOwnProperty.call(SAMPLE, name) ? SAMPLE[name] : "";
|
||||
});
|
||||
}
|
||||
|
||||
function unknownTokens(text) {
|
||||
var found, seen = {}, out = [];
|
||||
while ((found = TOKEN_RE.exec(text)) !== null) {
|
||||
var name = "{" + found[1] + "}";
|
||||
if (TOKENS.indexOf(name) === -1 && !seen[name]) { seen[name] = true; out.push(name); }
|
||||
}
|
||||
TOKEN_RE.lastIndex = 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
function cardOf(el) { return el ? el.closest("[data-smsx-card]") : null; }
|
||||
|
||||
function refresh(card) {
|
||||
var ta = card.querySelector("[data-smsx-text]");
|
||||
if (!ta) return;
|
||||
var text = ta.value;
|
||||
var shown = fillSample(text);
|
||||
var info = measure(shown);
|
||||
|
||||
var counter = card.querySelector("[data-smsx-counter]");
|
||||
var segments = card.querySelector("[data-smsx-segments]");
|
||||
var warn = card.querySelector("[data-smsx-warn]");
|
||||
var preview = card.querySelector("[data-smsx-preview]");
|
||||
var badge = card.querySelector("[data-smsx-badge]");
|
||||
|
||||
counter.textContent = toFa(info.len) + " نویسه";
|
||||
|
||||
if (info.parts === 0) {
|
||||
segments.textContent = "متن خالی است";
|
||||
segments.className = "smsx-metric";
|
||||
} else {
|
||||
segments.textContent = toFa(info.parts) + (info.parts === 1 ? " پیامک" : " پیامک (چندبخشی)");
|
||||
segments.className = "smsx-metric " + (info.parts <= 1 ? "is-ok" : (info.parts <= 3 ? "is-warn" : "is-bad"));
|
||||
}
|
||||
if (badge) badge.textContent = info.parts === 0 ? "—" : toFa(info.parts) + " پیامک";
|
||||
|
||||
var unknown = unknownTokens(text);
|
||||
if (unknown.length) {
|
||||
warn.hidden = false;
|
||||
warn.textContent = "متغیر ناشناخته: " + unknown.join("، ") + " — خالی رندر میشود.";
|
||||
} else {
|
||||
warn.hidden = true;
|
||||
warn.textContent = "";
|
||||
}
|
||||
|
||||
if (preview) preview.textContent = shown;
|
||||
}
|
||||
|
||||
var DIRTY = false;
|
||||
function markDirty(card) {
|
||||
if (!card || card.classList.contains("is-dirty")) return;
|
||||
card.classList.add("is-dirty");
|
||||
var flag = card.querySelector("[data-smsx-dirty]");
|
||||
if (flag) flag.hidden = false;
|
||||
DIRTY = true;
|
||||
}
|
||||
|
||||
function insertAtCursor(ta, token) {
|
||||
ta.focus();
|
||||
var value = ta.value;
|
||||
var start = ta.selectionStart == null ? value.length : ta.selectionStart;
|
||||
var end = ta.selectionEnd == null ? value.length : ta.selectionEnd;
|
||||
var before = value.slice(0, start);
|
||||
var prefix = (before && !/\s$/.test(before)) ? " " : "";
|
||||
ta.value = before + prefix + token + value.slice(end);
|
||||
var pos = start + prefix.length + token.length;
|
||||
ta.setSelectionRange(pos, pos);
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
// بستن اعلانها (Bulma JS بارگذاری نشده است).
|
||||
var dismiss = event.target.closest(".notification .delete");
|
||||
if (dismiss) {
|
||||
var box = dismiss.closest(".notification");
|
||||
if (box) box.remove();
|
||||
return;
|
||||
}
|
||||
var insert = event.target.closest("[data-smsx-insert]");
|
||||
if (insert) {
|
||||
event.preventDefault();
|
||||
var card = cardOf(insert);
|
||||
var ta = card && card.querySelector("[data-smsx-text]");
|
||||
if (!ta) return;
|
||||
insertAtCursor(ta, insert.getAttribute("data-smsx-insert"));
|
||||
refresh(card);
|
||||
markDirty(card);
|
||||
return;
|
||||
}
|
||||
if (event.target.closest("[data-smsx-expand-all]")) {
|
||||
document.querySelectorAll("[data-smsx-card]").forEach(function (d) { d.open = true; refresh(d); });
|
||||
return;
|
||||
}
|
||||
if (event.target.closest("[data-smsx-collapse-all]")) {
|
||||
document.querySelectorAll("[data-smsx-card]").forEach(function (d) { d.open = false; });
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("input", function (event) {
|
||||
var card = cardOf(event.target);
|
||||
if (!card) return;
|
||||
if (event.target.matches("[data-smsx-text]")) {
|
||||
refresh(card);
|
||||
markDirty(card);
|
||||
} else if (event.target.matches('input[name="title"], input[name="timing_value"]')) {
|
||||
markDirty(card);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("change", function (event) {
|
||||
var input = event.target;
|
||||
if (!input.matches('input[name="is_enabled"], input[name="auto_send"]')) return;
|
||||
var card = cardOf(input);
|
||||
if (!card) return;
|
||||
markDirty(card);
|
||||
if (input.name === "is_enabled") card.classList.toggle("is-off", !input.checked);
|
||||
// متن سوئیچِ همان کلید بهروز میشود (نه اولین سوئیچ کارت).
|
||||
var sw = input.closest(".smsx-switch");
|
||||
var label = sw && sw.querySelector(".smsx-switch__text");
|
||||
if (label) {
|
||||
label.textContent = input.name === "is_enabled"
|
||||
? (input.checked ? "فعال" : "غیرفعال")
|
||||
: (input.checked ? "خودکار" : "با تأیید مدیر");
|
||||
}
|
||||
});
|
||||
|
||||
// «toggle» رویداد حبابشو نیست؛ با capture گرفته میشود.
|
||||
document.addEventListener("toggle", function (event) {
|
||||
if (event.target.matches && event.target.matches("[data-smsx-card]") && event.target.open) {
|
||||
refresh(event.target);
|
||||
}
|
||||
}, true);
|
||||
|
||||
document.addEventListener("submit", function () { DIRTY = false; });
|
||||
|
||||
window.addEventListener("beforeunload", function (event) {
|
||||
if (!DIRTY) return;
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
});
|
||||
|
||||
function init() {
|
||||
document.querySelectorAll("[data-smsx-card]").forEach(function (card) {
|
||||
var toggle = card.querySelector('input[name="is_enabled"]');
|
||||
if (toggle) card.classList.toggle("is-off", !toggle.checked);
|
||||
refresh(card);
|
||||
});
|
||||
|
||||
// پس از ذخیره، همان کارت باز و در نمای دید باقی میماند (لنگر #type-…).
|
||||
var hash = window.location.hash;
|
||||
if (hash.indexOf("#type-") === 0) {
|
||||
var target = document.getElementById(hash.slice(1));
|
||||
if (target) {
|
||||
document.querySelectorAll("[data-smsx-card][open]").forEach(function (d) {
|
||||
if (d !== target) d.open = false;
|
||||
});
|
||||
target.open = true;
|
||||
refresh(target);
|
||||
target.scrollIntoView({ block: "start" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
{% endverbatim %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user