This commit is contained in:
javad committed 2026-10-11 19:04:20 +03:30
1 parent 1dea3509a7
commit e3a6d1f9d4
42 files changed
+33086 -15779

No files matched your search

@@ -0,0 +1,144 @@
{% load custom_tags %}
{% comment %}
کارت پروفایل مشتری — ستون سمت راست صفحهٔ مشتری.
تک‌منبع (DRY): هم customerBase.html و هم customerBaseNew.html این را include می‌کنند
تا ساختار، استایل و اسکریپت ویرایش/شماره‌ها در همهٔ صفحات مشتری یکسان بماند.
شناسه‌های موردنیاز جاوااسکریپت:
#formcsEdit, #enEdit, #editCs, #cancelEdit, #del_customer, #showUserDT, #userDT
{% endcomment %}
<form action="{% url 'admin_panel:customerEdit' customer.id %}" id="formcsEdit" class="has-background-white brad5" method="post">
{% csrf_token %}
<div class="buttons m-0 level p-2 is-hidden-tablet">
<p class="level-item">{{ customer.name_family }}</p>
<span id="showUserDT" class="button level-item">مشاهده مشخصات</span>
</div>
<div id="userDT" class="is-hidden-mobile">
<div class="field column is-12-desktop is-12-mobile m-0">
<label class="label">نام ونام خانوادگی</label>
<div class="control">
<input required disabled value="{{ customer.name_family }}" name="an_name" class="input an_name" type="text" placeholder="نام نام خانوادگی">
</div>
</div>
<div class="field column is-12-desktop is-12-mobile m-0">
<label class="label">آدرس</label>
<div class="control">
<input disabled name="an_address" value="{{ customer.address }}" class="input an_address" type="text" placeholder="آدرس کامل">
</div>
</div>
{% include "./_customer_phones.html" with phones_editable=1 %}
<div class="field column is-12-desktop is-12-mobile m-0">
<label class="label">تاریخ نصب</label>
<div class="control">
<input disabled name="installDate" value="{{ customer.installDate | toJalali }}" id="persianDateP" class="input ss02 installDate" type="text" placeholder="مثل: 1400/06/01">
</div>
</div>
<div class="field column is-12-desktop is-12-mobile m-0">
<label class="label">کدشناسایی</label>
<div class="control">
<input disabled value="{{ customer.idCode }}" name="idnCode" class="input idnCode ss02" type="text" placeholder="">
</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">
<select name="laghv_bazdid" style="width: 100%" class="select" id="LaghvBazdid">
<option value="false" {% if not customer.laghv_bazdid %}selected{% endif %}>خیر</option>
<option value="true" {% if customer.laghv_bazdid %}selected{% endif %}>بله</option>
</select>
</div>
</div>
<div class="field column is-12-desktop is-12-mobile m-0">
{% if perms.admin_panel.DelCustomer %}
<span class="button is-danger" id="del_customer">حذف مشتری</span>
{% endif %}
<span class="button is-success" id="enEdit">ویرایش</span>
<span class="button is-light is-hidden" id="cancelEdit">انصراف</span>
<input class="button is-danger is-hidden" id="editCs" value="ثبت" type="submit">
</div>
</div>
</form>
<script>
// اسکریپت مشترک کارت پروفایل مشتری: ویرایش/انصراف، افزودن و حذف شماره،
// باز/بستن در موبایل و حذف مشتری. (یک‌بار bind می‌شود.)
(function () {
if (window.__customerInfoCardInit) { return; }
window.__customerInfoCardInit = true;
// --- ورود به حالت ویرایش ---
$(document).on('click', '#enEdit', function () {
$('#formcsEdit').find('input, textarea, select').prop('disabled', false);
$('#formcsEdit').find('.remove-phone-btn, .add-phone-btn').prop('disabled', false);
$('#enEdit').addClass('is-hidden');
$('#cancelEdit').removeClass('is-hidden');
$('#editCs').removeClass('is-hidden');
});
// --- انصراف: بارگذاری مجدد؛ تغییرات ذخیره‌نشده دور ریخته می‌شود ---
$(document).on('click', '#cancelEdit', function () {
window.location.reload();
});
// --- افزودن شماره (در حالت ویرایش) ---
$(document).on('click', '.add-phone-btn', function () {
var phoneRow = `
<div class="field has-addons mt-2 ss02">
<div class="control" style="width: 30%;">
<input name="phone_titles" class="input" type="text" 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>
</div>`;
$('#dynamic-phones-edit').append(phoneRow);
});
// --- حذف شماره (حداقل یک شماره باید بماند) ---
$(document).on('click', '.remove-phone-btn', function () {
if ($('#dynamic-phones-edit .field.has-addons').length > 1) {
$(this).closest('.field.has-addons').remove();
} else {
window.alert('حداقل یک شمارهٔ تماس باید وجود داشته باشد.');
}
});
// --- باز/بستن مشخصات در موبایل ---
$(document).on('click', '#showUserDT', function () {
var $btn = $(this);
if ($btn.text().trim() === 'مشاهده مشخصات') {
$btn.text('مخفی کردن');
$('#userDT').removeClass('is-hidden-mobile');
} else {
$btn.text('مشاهده مشخصات');
$('#userDT').addClass('is-hidden-mobile');
}
});
// --- حذف مشتری ---
$(document).on('click', '#del_customer', function () {
if (window.confirm('با حذف مشتری، تمام سوابق، فاکتورها و اطلاعات از سیستم حذف می‌شود، مطمئن هستید؟')) {
window.location.href = '/panel/customer/{{ customer.id }}/del';
}
});
})();
</script>
@@ -19,6 +19,16 @@
📞
</a>
</div>
{% if perms.admin_panel.ManageSMS and entry.is_mobile %}
<div class="control">
<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>
</div>
{% endif %}
{% if phones_editable and perms.admin_panel.ViewStore %}
<div class="control">
<button type="button" disabled class="button is-danger remove-phone-btn">-</button>
@@ -35,3 +45,50 @@
</button>
{% endif %}
</div>
{% if perms.admin_panel.ManageSMS %}
<script>
// تعیین شمارهٔ اصلی پیامک از همین ستون (بدون فرم تودرتو) — با درخواست async و
// بارگذاری مجدد صفحه تا وضعیت شماره‌ها و رنگ دکمه‌ها به‌درستی به‌روز شوند.
(function () {
if (window.__smsPrimaryToggleInit) { return; }
window.__smsPrimaryToggleInit = true;
function csrfToken() {
var match = document.cookie.match(/(^|;\s*)csrftoken=([^;]+)/);
return match ? decodeURIComponent(match[2]) : '';
}
document.addEventListener('click', function (event) {
var btn = event.target.closest ? event.target.closest('.sms-primary-toggle') : null;
if (!btn || btn.disabled) { return; }
event.preventDefault();
var body = new URLSearchParams();
body.append('phone', btn.getAttribute('data-phone') || '');
body.append('is_primary', btn.getAttribute('data-primary') === 'true' ? 'false' : 'true');
btn.classList.add('is-loading');
btn.disabled = true;
fetch(btn.getAttribute('data-url'), {
method: 'POST',
credentials: 'same-origin',
headers: {
'X-CSRFToken': csrfToken(),
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: body.toString()
}).then(function (response) {
if (!response.ok) { throw new Error('HTTP ' + response.status); }
window.location.reload();
}).catch(function () {
btn.classList.remove('is-loading');
btn.disabled = false;
window.alert('خطا در تغییر شمارهٔ اصلی. لطفاً دوباره تلاش کنید.');
});
});
})();
</script>
{% endif %}
@@ -3,37 +3,7 @@
<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">
<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">
@@ -58,7 +28,7 @@
{% csrf_token %}
<div class="field">
<div class="control">
<textarea class="textarea" name="text" rows="2" placeholder="متن پیامک... (خالی بگذارید تا متن قالب «پیام سفارشی» استفاده شود)"></textarea>
<textarea class="textarea" name="text" rows="2" placeholder="متن پیامک..."></textarea>
</div>
</div>
<p class="help">می‌توانید از متغیرهایی مثل {name} (نام مشتری) استفاده کنید.</p>
+8 -1
View File
@@ -120,19 +120,26 @@
{% if perms.admin_panel.ManageSMS %}
{% if perms.admin_panel.ManageSMS or perms.admin_panel.ManageSMSSettings %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">پیامک</a>
<div class="navbar-dropdown">
{% if perms.admin_panel.ManageSMSSettings %}
<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>
{% endif %}
{% if perms.admin_panel.ManageSMS %}
<a href="{% url 'admin_panel:sms_queue' %}" style="padding-right: 10px;" class="navbar-item">
صف ارسال
</a>
<a href="{% url 'admin_panel:sms_report' %}" style="padding-right: 10px;" class="navbar-item">
گزارش ارسال
</a>
{% endif %}
</div>
</div>
{% endif %}
@@ -6,55 +6,7 @@
<div class="">
<div class="columns m-0">
<div class="column is-12-mobile is-4-desktop">
<form action="{% url 'admin_panel:customerEdit' customer.id %}" id="formcsEdit" class="has-background-white brad5" method="post">
{% csrf_token %}
<div class="buttons m-0 level p-2 is-hidden-tablet">
<p class="level-item">محمد جواد فصاحت</p>
<span id="showUserDT" class="button level-item">مشاهده مشخصات</span>
</div>
<div id="userDT" class="is-hidden-mobile">
<div class="field column is-12-desktop is-12-mobile m-0">
<label class="label">نام ونام خانوادگی</label>
<div class="control">
<input disabled value="{{ customer.name_family }}" name="an_name" class="input an_name" type="text" placeholder="نام نام خانوادگی">
</div>
</div>
<div class="field column is-12-desktop is-12-mobile m-0">
<label class="label">آدرس</label>
<div class="control">
<input disabled name="an_address" value="{{ customer.address }}" class="input an_address" type="text" placeholder="آدرس کامل">
</div>
</div>
{% include "./_customer_phones.html" %}
<div class="field column is-12-desktop is-12-mobile m-0">
<label class="label">تاریخ نصب</label>
<div class="control">
<input disabled name="installDate" value="{{ customer.installDate | toJalali }}" id="persianDateP" class="input installDate" type="text" placeholder="مثل: 1400/06/01">
</div>
</div>
<div class="field column is-12-desktop is-12-mobile m-0">
<label class="label">کدشناسایی</label>
<div class="control">
<input disabled value="{{ customer.idCode }}" name="idnCode" class="input idnCode" type="text" placeholder="">
</div>
</div>
<div class="field column is-12-desktop is-12-mobile m-0">
{% if perms.admin_panel.DelCustomer %}
<span class="button is-danger" id="del_customer">حذف مشتری</span>
{% endif %}
<span class="button is-success" id="enEdit">ویرایش</span>
<input class="button is-danger is-hidden" id="editCs" value="ثبت" type="submit">
</div>
</div>
</form>
{% include "./_customer_info.html" %}
</div>
<div class="column is-12-mobile is-8-desktop">
@@ -229,11 +181,6 @@
});
$("#del_customer").click(function(){
if(confirm("با حذف مشتری، تمام سوابق، فاکتورها و اطلاعات از سیستم حذف میشود، مطمئن هستید؟")){
window.location.href = "/panel/customer/{{ customer.id }}/del";
}
});
$(".closeNewIteam").click(function(){
$("#addNewIteam").removeClass("is-active");
@@ -318,22 +265,6 @@ $(document).ready(function() {
var test = 0
$("#enEdit").click(function(){
$("#formcsEdit").find("input").prop('disabled', false);
$("#enEdit").addClass("is-hidden");
$("#editCs").removeClass("is-hidden");
});
$("#showUserDT").click(function(){
if( $(this).html()=='مشاهده مشخصات' ){
$(this).html('مخفی کردن');
$("#userDT").removeClass("is-hidden-mobile");
}else{
$(this).html('مشاهده مشخصات');
$("#userDT").addClass("is-hidden-mobile");
}
});
$(".closeSearch").click(function(){
$(".SearchBox").removeClass("is-active")
});
@@ -18,76 +18,7 @@
<div class="">
<div class="columns m-0">
<div class="column is-12-mobile is-4-desktop">
<form action="{% url 'admin_panel:customerEdit' customer.id %}" id="formcsEdit" class="has-background-white brad5" method="post">
{% csrf_token %}
<div class="buttons m-0 level p-2 is-hidden-tablet">
<p class="level-item">
{{ customer.name_family }}
</p>
<span id="showUserDT" class="button level-item">مشاهده مشخصات</span>
</div>
<div id="userDT" class="is-hidden-mobile">
<div class="field column is-12-desktop is-12-mobile m-0">
<label class="label">نام ونام خانوادگی</label>
<div class="control">
<input required disabled value="{{ customer.name_family }}" name="an_name" class="input an_name" type="text" placeholder="نام نام خانوادگی">
</div>
</div>
<div class="field column is-12-desktop is-12-mobile m-0">
<label class="label">آدرس</label>
<div class="control">
<input disabled name="an_address" value="{{ customer.address }}" class="input an_address" type="text" placeholder="آدرس کامل">
</div>
</div>
{% include "./_customer_phones.html" with phones_editable=1 %}
<div class="field column is-12-desktop is-12-mobile m-0">
<label class="label">تاریخ نصب</label>
<div class="control">
<input disabled name="installDate" value="{{ customer.installDate | toJalali }}" id="persianDateP" class="input ss02 installDate" type="text" placeholder="مثل: 1400/06/01">
</div>
</div>
<div class="field column is-12-desktop is-12-mobile m-0">
<label class="label">کدشناسایی</label>
<div class="control">
<input disabled value="{{ customer.idCode }}" name="idnCode" class="input idnCode ss02 " type="text" placeholder="">
</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">
<select name="laghv_bazdid" style="width: 100%" class="select" id="LaghvBazdid">
<option value="false">خیر</option>
<option value="true">بله</option>
</select>
</div>
</div>
<div class="field column is-12-desktop is-12-mobile m-0">
{% if perms.admin_panel.DelCustomer %}
<span class="button is-danger" id="del_customer">حذف مشتری</span>
{% endif %}
<span class="button is-success" id="enEdit">ویرایش</span>
<input class="button is-danger is-hidden" id="editCs" value="ثبت" type="submit">
</div>
</div>
</form>
{% include "./_customer_info.html" %}
</div>
<div class="column is-12-mobile is-8-desktop">
@@ -102,6 +33,7 @@
<li id="newFactorOpen" class="{% if url_name == 'customerNew' %}is-active{% else %} {% endif %} "><a href="{% url 'admin_panel:customerNew' customer.id %}">ایجاد فاکتور</a></li>
<li id="FactorHaOpen" class="{% if url_name == 'customerFactors' %}is-active{% else %} {% endif %} "><a href="{% url 'admin_panel:customerFactors' customer.id %}">فاکتورها </a></li>
<li class=""><a href="{% url 'admin_panel:customerAction' customer.id %}">بازاریابی</a></li>
{% if perms.admin_panel.ManageSMS %}<li class="{% if url_name == 'customerSmsHistory' %}is-active{% else %} {% endif %} "><a href="{% url 'admin_panel:customerSmsHistory' customer.id %}">سوابق پیامک</a></li>{% endif %}
{% endwith %}
@@ -473,11 +405,6 @@
});
$("#del_customer").click(function(){
if(confirm("با حذف مشتری، تمام سوابق، فاکتورها و اطلاعات از سیستم حذف میشود، مطمئن هستید؟")){
window.location.href = "/panel/customer/{{ customer.id }}/del";
}
});
$(".closeNewIteam").click(function(){
$("#addNewIteam").removeClass("is-active");
@@ -603,50 +530,6 @@ $(document).ready(function() {
var test = 0;
$("#enEdit").click(function(){
$("#formcsEdit").find("input, textarea").prop('disabled', false);
// فعال کردن دکمه‌های کنترل تلفن
$("#formcsEdit").find("button.remove-phone-btn, button.add-phone-btn").prop('disabled', false);
$("#enEdit").addClass("is-hidden");
$("#editCs").removeClass("is-hidden");
});
// توابع داینامیک فرم ویرایش
$(document).on('click', '.add-phone-btn', function() {
var phoneRow = `
<div class="field has-addons mt-2 ss02">
<div class="control" style="width: 35%;">
<input name="phone_titles" class="input" type="text" 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>
</div>`;
$("#dynamic-phones-edit").append(phoneRow);
});
$(document).on('click', '.remove-phone-btn', function() {
if ($("#dynamic-phones-edit .has-addons").length > 1) {
$(this).closest('.field.has-addons').remove();
} else {
alert("حداقل یک شماره تماس باید وجود داشته باشد.");
}
});
$("#showUserDT").click(function(){
if( $(this).html()=='مشاهده مشخصات' ){
$(this).html('مخفی کردن');
$("#userDT").removeClass("is-hidden-mobile");
}else{
$(this).html('مشاهده مشخصات');
$("#userDT").addClass("is-hidden-mobile");
}
});
$(".closeSearch").click(function(){
$(".SearchBox").removeClass("is-active")
});
@@ -0,0 +1,137 @@
{% extends "./customerBaseNew.html" %}
{% load custom_tags %}
{% load static %}
{% block BodyCustomer1 %}
<style>
/* جدول سوابق پیامک مشتری */
.smshist-msg {
white-space: pre-wrap;
overflow-wrap: anywhere;
max-width: 34rem;
line-height: 1.9;
}
.smshist-meta { font-size: .78rem; line-height: 1.7; }
.smshist-badge { white-space: nowrap; }
</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="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 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 %}
</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>
{% if items.has_other_pages %}
<nav class="pagination is-centered" 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 %}
<a class="pagination-previous" disabled>قبلی</a>
{% endif %}
{% if items.has_next %}
<a class="pagination-next" href="?page={{ items.next_page_number }}{% if status %}&status={{ status }}{% endif %}{% if delivery %}&delivery={{ delivery }}{% endif %}">بعدی</a>
{% else %}
<a class="pagination-next" disabled>بعدی</a>
{% endif %}
<ul class="pagination-list">
<li><span class="pagination-link">صفحهٔ {{ items.number }} از {{ items.paginator.num_pages }}</span></li>
</ul>
</nav>
{% endif %}
</div>
{% endblock %}
@@ -12,6 +12,9 @@
حذف یک مورد، فقط همان نوبت را کنسل می‌کند. یادآوری‌های دوره‌ای (بازدید و ثبت
اطلاعات چک) در <strong>دورهٔ بعدی</strong> دوباره ساخته می‌شوند؛ پس با حذف یک
مورد، پیامک برای آن مشتری برای همیشه قطع نمی‌شود.
<br>
پیامک‌های <strong>ارسال‌شده</strong> از این صف بیرون می‌روند؛ فهرست و سابقهٔ
کامل‌شان در <a href="{% url 'admin_panel:sms_report' %}">گزارش ارسال</a> است.
</div>
<div class="columns is-multiline mb-2" dir="rtl">
@@ -21,6 +24,13 @@
<p class="title is-5 mb-0">{{ total_count }}</p>
</div>
</div>
<div class="column is-narrow">
<a href="{% url 'admin_panel:sms_report' %}" class="box py-3 px-4 has-text-centered"
title="مشاهدهٔ سوابق ارسال‌شده‌ها در گزارش ارسال">
<p class="heading mb-0">ارسال‌شده (در گزارش)</p>
<p class="title is-5 mb-0 has-text-success">{{ sent_count }}</p>
</a>
</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"
@@ -71,11 +81,12 @@
</form>
<div class="notification is-danger is-light is-flex is-align-items-center is-justify-content-space-between mb-3">
<span>خالی کردن کل صف: همهٔ پیامک‌ها (در هر وضعیتی) حذف می‌شوند و دیگر بازنمی‌گردند.</span>
<span>خالی کردن صف: همهٔ موارد باقی‌مانده (در انتظار تأیید، تأییدشده، در حال پردازش و ناموفق)
حذف می‌شوند و دیگر بازنمی‌گردند. پیامک‌های ارسال‌شده دست‌نخورده می‌مانند (در گزارش ارسال).</span>
<form method="post" action="{% url 'admin_panel:sms_queue_empty' %}"
onsubmit="return confirm('کل صف پیامک خالی شود؟ این پیامک‌ها دیگر هرگز ارسال یا بازسازی نمی‌شوند.');">
onsubmit="return confirm('صف پیامک خالی شود؟ این پیامک‌ها دیگر هرگز ارسال یا بازسازی نمی‌شوند.');">
{% csrf_token %}
<button class="button is-danger" type="submit">خالی کردن کل صف</button>
<button class="button is-danger" type="submit">خالی کردن صف</button>
</form>
</div>
@@ -119,11 +130,17 @@
<button class="button is-small is-warning" type="submit">ارسال مجدد</button>
</form>
{% endif %}
{% if item.status != 'sent' %}
<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>
{% else %}
<a class="button is-small is-light"
href="{% url 'admin_panel:sms_report' %}?q={{ item.phone_number }}"
title="این پیامک ارسال شده است؛ سابقهٔ آن در گزارش ارسال است">در گزارش</a>
{% endif %}
</div>
</td>
</tr>
@@ -0,0 +1,189 @@
{% extends "./base.html" %}
{% load custom_tags %}
{% block bodyPage %}
<div class="container mt-5" dir="rtl">
<h1 class="title is-4">گزارش ارسال پیامک</h1>
<p class="has-text-grey mb-3">
پیامک‌های <strong>تأییدشده و ارسال‌شده</strong> (موارد «در انتظار تأیید» و «لغو‌شده»
در این گزارش نمی‌آیند) به‌همراه «تعداد پیامکی که اپراتور محاسبه کرده»
(بر اساس کدگذاری GSM-7 برای انگلیسی/عدد و UCS-2 برای فارسی/یونیکد) — برای تحلیل هزینه.
</p>
{# --- کارت‌های خلاصه (روی کل نتیجهٔ فیلترشده) --- #}
<div class="columns is-multiline mb-2">
<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_records }}</p>
</div>
</div>
<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 has-text-danger">{{ total_parts }}</p>
</div>
</div>
<div class="column is-narrow">
<div class="box py-3 px-4 has-text-centered">
<p class="heading mb-0">GSM-7 / UCS-2</p>
<p class="title is-6 mb-0">{{ gsm7_parts }} / {{ ucs2_parts }}</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>
{# --- فرم فیلتر --- #}
<form method="get" class="box">
<div class="columns is-multiline is-variable is-2">
<div class="column is-2">
<label class="label is-small">از تاریخ (شمسی)</label>
<input class="input" type="text" id="dateFromP" name="date_from"
value="{{ date_from }}" placeholder="1404/01/01" autocomplete="off">
</div>
<div class="column is-2">
<label class="label is-small">تا تاریخ (شمسی)</label>
<input class="input" type="text" id="dateToP" name="date_to"
value="{{ date_to }}" placeholder="1404/12/29" autocomplete="off">
</div>
<div class="column is-2">
<label class="label is-small">وضعیت ارسال</label>
<div class="select is-fullwidth">
<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="column is-2">
<label class="label is-small">وضعیت تحویل</label>
<div class="select is-fullwidth">
<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="column is-2">
<label class="label is-small">نوع پیامک</label>
<div class="select is-fullwidth">
<select name="sms_type">
<option value="">همه</option>
{% for value, label in type_choices %}
<option value="{{ value }}" {% if sms_type == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="column is-2">
<label class="label is-small">جستجو (متن/شماره/نام مشتری)</label>
<input class="input" type="text" name="q" value="{{ query }}" placeholder="جستجو…">
</div>
</div>
<div class="buttons">
<button class="button is-link" type="submit">اعمال فیلتر</button>
{% if has_filter %}
<a class="button is-light" href="{% url 'admin_panel:sms_report' %}">حذف فیلترها</a>
{% endif %}
</div>
</form>
{# --- جدول --- #}
<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: 16rem;">متن دقیق پیامک</th>
<th style="min-width: 8rem;">شماره گیرنده</th>
<th>نوع پیامک</th>
<th style="min-width: 8rem;">مشتری</th>
<th>سوابق</th>
<th>تعداد پیامک</th>
<th>وضعیت ارسال</th>
<th>تحویل</th>
<th style="min-width: 8rem;">تاریخ ایجاد</th>
<th style="min-width: 8rem;">تاریخ ارسال</th>
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
<td>{{ row.item.id }}</td>
<td class="smsrep-msg">{{ row.item.message }}</td>
<td dir="ltr">{{ row.item.phone_number }}</td>
<td>{{ row.item.get_sms_type_display|default:'دستی/سفارشی' }}</td>
<td>
{% if row.item.customer_id %}
<a href="{% url 'admin_panel:customer' row.item.customer_id %}">{{ row.item.customer.name_family }}</a>
{% else %}—{% endif %}
</td>
<td>
{% if row.item.customer_id %}
<a class="button is-small is-light"
href="{% url 'admin_panel:customerSmsHistory' row.item.customer_id %}"
title="مشاهدهٔ سوابق پیامک این مشتری">سوابق پیامک</a>
{% else %}—{% endif %}
</td>
<td class="has-text-centered">
<span class="tag {% if row.parts > 1 %}is-danger{% else %}is-success{% endif %} is-light"
title="{{ row.encoding }} · {{ row.length }} واحد">{{ row.parts }}</span>
</td>
<td>{{ row.item.get_status_display }}</td>
<td>{{ row.item.get_delivery_status_display|default:'—' }}</td>
<td>{{ row.item.created_at|toJalaliDT }}</td>
<td>{{ row.item.sent_at|toJalaliDT|default:'—' }}</td>
</tr>
{% empty %}
<tr><td colspan="11" class="has-text-centered">پیامکی با این شرایط یافت نشد.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if page.has_other_pages %}
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
{% if page.has_previous %}
<a class="pagination-previous" href="?{{ base_query }}&page={{ page.previous_page_number }}">قبلی</a>
{% else %}<a class="pagination-previous" disabled>قبلی</a>{% endif %}
{% if page.has_next %}
<a class="pagination-next" href="?{{ base_query }}&page={{ page.next_page_number }}">بعدی</a>
{% else %}<a class="pagination-next" disabled>بعدی</a>{% endif %}
<ul class="pagination-list">
<li><span class="pagination-link">صفحهٔ {{ page.number }} از {{ page.paginator.num_pages }}</span></li>
</ul>
</nav>
{% endif %}
</div>
{% endblock %}
{% block cssEnd %}
<style>
/* متن پیامک با حفظ شکست خطوط و شکستن متن‌های طولانی/بدون‌فاصله */
.smsrep-msg { white-space: pre-wrap; overflow-wrap: anywhere; max-width: 26rem; line-height: 1.9; }
</style>
{% endblock %}
{% block jsEnd %}
<script>
(function () {
if (typeof kamaDatepicker !== 'function') { return; }
var opts = { buttonsColor: "red", forceFarsiDigits: true, sync: true };
['dateFromP', 'dateToP'].forEach(function (id) {
if (document.getElementById(id)) { kamaDatepicker(id, opts); }
});
})();
</script>
{% endblock %}
@@ -7,6 +7,13 @@
<div class="box">
<form method="post" action="{% url 'admin_panel:sms_settings' %}">
{% csrf_token %}
<h2 class="title is-5">اتصال به سامانه پیامک</h2>
<p class="help mb-3">
این بخش مشخص می‌کند پیامک‌ها <strong>از چه مسیری و با چه حساب کاربری</strong> به
اپراتور سپرده می‌شوند. تا وقتی «وضعیت ارسال» روشن نباشد، هیچ‌کدام از سرویس‌های
پس‌زمینه (worker) پیامکی نمی‌فرستند.
</p>
<div class="columns is-multiline">
<div class="field column is-4-desktop is-12-mobile">
<label class="label">سرویس‌دهنده</label>
@@ -19,6 +26,19 @@
</select>
</div>
</div>
<p class="help">
<strong>مسیر فنی ارسال را تعیین می‌کند</strong> و روی بقیهٔ فیلدها اثر دارد:
<br>• <span dir="ltr">0098SMS (لینکی HTTP)</span>: ساده‌ترین حالت؛ درخواست به
آدرس <span dir="ltr">api_url</span> زده می‌شود. <strong>شناسهٔ پیامک برنمی‌گردد</strong>،
پس استعلام «تحویل به گوشی» ممکن نیست.
<br>• <span dir="ltr">0098SMS (وب‌سرویس)</span>: ارسال SOAP به
<span dir="ltr">webservice_url</span>؛ <strong>شناسهٔ پیامک (smsid)</strong> برمی‌گردد و
می‌توان وضعیت واقعی تحویل را استعلام کرد.
<br>• <span dir="ltr">HTTP عمومی</span>: برای سرویس‌های دلخواه؛ با
<span dir="ltr">api_token</span> احراز هویت می‌شود.
<br>• <span dir="ltr">Console</span>: فقط برای تست/دیباگ؛ پیامک واقعی نمی‌رود و
متن در لاگ چاپ می‌شود.
</p>
</div>
<div class="field column is-4-desktop is-12-mobile">
@@ -29,6 +49,11 @@
فعال باشد
</label>
</div>
<p class="help">
<strong>کلید اصلی خاموش/روشن ارسال.</strong> اگر تیک نخورد، سرویس worker هیچ
پیامکی نمی‌فرستد (ردیف‌ها در صف می‌مانند). اسکنر و صف‌کردن مستقل از این است،
پس می‌توانید پیامک‌ها را از قبل در صف بچینید و بعد ارسال را روشن کنید.
</p>
</div>
<div class="field column is-4-desktop is-12-mobile">
@@ -36,6 +61,13 @@
<div class="control">
<input class="input" type="number" name="timeout_seconds" value="{{ config.timeout_seconds }}">
</div>
<p class="help">
<strong>حداکثر زمان انتظار worker برای پاسخ سرویس‌دهنده</strong> در هر تلاش.
اگر پاسخ دیرتر بیاید، آن تلاش «ناموفق» ثبت می‌شود و بر اساس سیاست تلاش مجدد
دوباره در نوبت قرار می‌گیرد. مقدار کم (مثل ۵) ممکن است در سرویس کند، ارسال‌های
سالم را هم ناموفق کند؛ مقدار زیاد (مثل ۱۲۰) در قطعی سرویس، worker را بیکار
نگه می‌دارد. پیش‌فرض ۳۰ ثانیه متعادل است.
</p>
</div>
<div class="field column is-12-mobile">
@@ -44,9 +76,14 @@
<input class="input" type="text" name="api_url" value="{{ config.api_url }}" dir="ltr">
</div>
<p class="help">
برای 0098SMS همین آدرس لینکی ارسال است
<strong>آدرس درخواست ارسال برای providerهای «لینکی HTTP» و «HTTP عمومی».</strong>
<br>• برای 0098SMS لینکی همین آدرس است
(<span dir="ltr">http://www.0098sms.com/sendsmslink.aspx</span>)؛ این سرویس
WSDL/SOAP ندارد و آدرس WSDL باعث خطای «Invalid XML content» می‌شود.
WSDL/SOAP ندارد و اگر آدرس WSDL بگذارید خطای «Invalid XML content» می‌گیرید.
<br>• برای provider وب‌سرویس، این فیلد استفاده <strong>نمی‌شود</strong> (آن یکی
<span dir="ltr">webservice_url</span> را می‌خواند).
<br>• اگر آدرس اشتباه یا در دسترس نباشد، پیامک‌ها «ناموفق» می‌شوند و در صف تلاش
مجدد می‌خورند.
</p>
</div>
@@ -56,8 +93,9 @@
<input class="input" type="text" name="webservice_url" value="{{ config.webservice_url }}" dir="ltr">
</div>
<p class="help">
ارسال با شناسهٔ پیامک (<span dir="ltr">smsid</span>) و استعلام تحویل به گوشی
از همین آدرس انجام می‌شود. پیش‌فرض:
<strong>فقط وقتی provider = «وب‌سرویس» معنا دارد.</strong> هم ارسال با شناسهٔ
پیامک (<span dir="ltr">smsid</span>) و هم استعلام تحویل به گوشی از همین آدرس
انجام می‌شود. با providerهای دیگر این فیلد نادیده گرفته می‌شود. پیش‌فرض:
<span dir="ltr">https://webservice.0098sms.com/service.asmx</span>
</p>
</div>
@@ -67,7 +105,11 @@
<div class="control">
<input class="input" type="text" name="panel_no" value="{{ config.panel_no }}" dir="ltr">
</div>
<p class="help">اگر خالی بماند از «شماره خط فرستنده» استفاده می‌شود.</p>
<p class="help">
<strong>شمارهٔ اختصاصی حساب شما در وب‌سرویس</strong> (فقط provider وب‌سرویس).
اگر خالی بماند، سیستم از «شماره خط فرستنده» استفاده می‌کند. مقدار اشتباه باعث
رد شدن ارسال با خطای احراز هویت می‌شود.
</p>
</div>
<div class="field column is-4-desktop is-12-mobile">
@@ -75,6 +117,10 @@
<div class="control">
<input class="input" type="text" name="username" value="{{ config.username }}" dir="ltr">
</div>
<p class="help">
<strong>نام کاربری حساب سامانهٔ پیامک</strong> (در همهٔ providerها به‌جز Console
لازم است). اگر اشتباه باشد، ارسال‌ها رد می‌شوند و در «خطا» صف ثبت می‌گردد.
</p>
</div>
<div class="field column is-4-desktop is-12-mobile">
@@ -82,6 +128,11 @@
<div class="control">
<input class="input" type="password" name="password" placeholder="{% if config.password %}(بدون تغییر)‌{% endif %}" dir="ltr">
</div>
<p class="help">
<strong>رمز حساب سامانهٔ پیامک.</strong> برای امنیت، مقدار ذخیره‌شده بازخوانی
نمی‌شود؛ این کادر را <strong>خالی بگذارید</strong> تا رمز قبلی بماند و فقط وقتی
رمز عوض شده، مقدار جدید را وارد کنید.
</p>
</div>
<div class="field column is-4-desktop is-12-mobile">
@@ -89,6 +140,12 @@
<div class="control">
<input class="input" type="text" name="sender" value="{{ config.sender }}" dir="ltr">
</div>
<p class="help">
<strong>شماره یا عنوان خطی که پیامک با آن به دست مشتری می‌رسد.</strong> باید
جزو خطوط مجاز حساب شما باشد؛ در غیر این صورت اپراتور ارسال را رد می‌کند.
همچنین در provider وب‌سرویس، اگر «شمارهٔ پنل» خالی باشد همین مقدار جای آن
استفاده می‌شود.
</p>
</div>
<div class="field column is-12-mobile">
@@ -96,15 +153,23 @@
<div class="control">
<input class="input" type="password" name="api_token" placeholder="{% if config.api_token %}(بدون تغییر)‌{% endif %}" dir="ltr">
</div>
<p class="help">
<strong>کلید/توکن احراز هویت providerهای «HTTP عمومی».</strong> برای 0098SMS
لازم نیست. مثل رمز، خالی گذاشتن آن یعنی «بدون تغییر».
</p>
</div>
</div>
<hr>
<h2 class="title is-5">کنترل جریان ارسال</h2>
<p class="help mb-3">
برای جلوگیری از سیل پیامک: تنها مواردی ارسال می‌شوند که پنجرهٔ یادآوری‌شان از
«خط شروع» به بعد باز شود. موارد قدیمی‌تر تنها با روشن‌کردن «اجازهٔ موارد قدیمی»
و در سقف تعیین‌شده ارسال می‌شوند. بین هر پیامک هم فاصلهٔ زمانی رعایت می‌شود.
این بخش تعیین می‌کند اسکنر دوره‌ای <strong>چه چیزی و چقدر</strong> در هر اجرا در صف
بگذارد. اسکنر هر چند دقیقه یک‌بار اجرا می‌شود و بر اساس «سررسیدها» تصمیم می‌گیرد
کدام مشتری باید یادآوری بگیرد. منطق بودجه به این شکل است:
<strong>سقف هر اجرا</strong> ← تعیین می‌کند کلاً چند مورد صف شود؛
<strong>خط شروع</strong> ← موارد قدیمی‌تر از آن را کنار می‌گذارد؛
<strong>اجازهٔ موارد قدیمی + سقف آن</strong> ← تصمیم می‌گیرد از آن موارد کنارگذاشته‌شده
چند تا (اگر لازم بود) ارسال شود.
</p>
<div class="columns is-multiline">
@@ -116,9 +181,15 @@
placeholder="مثلاً 1403/07/18 — خالی = همین حالا" dir="ltr">
</div>
<p class="help">
فقط یادآوری‌هایی صف می‌شوند که «پنجرهٔ» آن‌ها از این تاریخ به بعد باز شده باشد.
مشتریان قدیمی‌تری که پنجره‌شان پیش از این تاریخ باز شده، وارد صف نمی‌شوند.
قالب: <span dir="ltr">YYYY/MM/DD</span> (شمسی) — خالی = همین حالا.
<strong>خط جداکنندهٔ «موارد جدید» از «موارد قدیمی».</strong>
هر مورد (چک، نسیه، بازدید) یک «لحظهٔ باز شدن پنجرهٔ یادآوری» دارد. اگر این
لحظه <strong>از این تاریخ به بعد</strong> باشد، مورد «جدید» است و آزادانه صف
می‌شود؛ اگر <strong>قبل از</strong> آن باشد، «قدیمی» (backlog) حساب می‌شود.
<br>• هدف اصلی: در نخستین راه‌اندازی، هزاران مشتری قدیمیِ واجد شرط یک‌باره پیامک
نگیرند.
<br>• قالب: <span dir="ltr">YYYY/MM/DD</span> شمسی — خالی = همین حالا.
<br>• عوض‌کردن آن به گذشته، مشتریان قدیمی‌تر را «جدید» می‌کند و ممکن است سیل پیامک
بسازد؛ با احتیاط تغییر دهید.
</p>
</div>
@@ -127,6 +198,19 @@
<div class="control">
<input class="input" type="number" min="0" name="max_per_run" value="{{ config.max_per_run }}">
</div>
<p class="help">
<strong>سقف کل صف‌کردن در یک اجرا — به‌ازای هر اسکنر جداگانه.</strong>
<br>• «هر اسکنر» یعنی این عدد برای هر نوع یادآوری مستقل اعمال می‌شود
(تأمین موجودی چک، سررسید نسیه، بازدید دورهای)؛ پس اگر ۱۰۰ باشد و هر سه اسکنر
واجد شرط باشند، در یک اجرا حداکثر ۱۰۰ + ۱۰۰ + ۱۰۰ = ۳۰۰ ردیف صف می‌شود.
<br>• شمارش از مجموع «جدیدها + موارد قدیمی» است، نه فقط جدیدها.
<br>• <strong>۰ = بی‌نهایت</strong> (هیچ سقفی اعمال نمی‌شود).
<br>• چون اسکنر دوره‌ای اجرا می‌شود، سقف باعث «دسته‌دسته» شدن ارسال‌های انبوه
می‌شود (بقیه در اجراهای بعدی می‌آیند) و از ریختن یک‌بارهٔ صدها پیامک روی ارسال
جلوگیری می‌کند.
<br>• نکته: این سقف‌ها روی «صف‌کردن» اثر دارند، نه سرعت ارسال واقعی؛ سرعت ارسال
به «فاصلهٔ بین هر پیامک» بستگی دارد.
</p>
</div>
<div class="field column is-3-desktop is-6-mobile">
@@ -134,6 +218,11 @@
<div class="control">
<input class="input" type="number" min="0" name="send_interval_seconds" value="{{ config.send_interval_seconds }}">
</div>
<p class="help">
<strong>مکث سرویس worker پس از هر ارسال</strong> تا اپراتور زیر بار زیاد نرود.
مثال: با مقدار ۳، ارسال ۱۰۰ پیامک حدود ۵ دقیقه طول می‌کشد. مقدار ۰ یعنی بدون
مکث (سریع ولی پرخطر — ممکن است سرویس‌دهنده محدودتان کند).
</p>
</div>
<div class="field column is-3-desktop is-6-mobile">
@@ -144,6 +233,12 @@
فعال باشد
</label>
</div>
<p class="help">
<strong>دربِ موارد «قدیمی‌تر از تاریخ شروع دوره».</strong> به‌صورت پیش‌فرض بسته
است. تا وقتی بسته باشد، موارد قدیمی <strong>هرگز</strong> صف نمی‌شوند (حتی اگر
سقف باقی باشد). اگر لازم بود این موارد هم پردازش شوند، آن را روشن کنید و مقدار
«حداکثر موارد قدیمی در هر اجرا» را تعیین کنید.
</p>
</div>
<div class="field column is-3-desktop is-6-mobile">
@@ -151,7 +246,15 @@
<div class="control">
<input class="input" type="number" min="0" name="backlog_max" value="{{ config.backlog_max }}">
</div>
<p class="help">صفر = هیچ مورد قدیمی ارسال نمی‌شود.</p>
<p class="help">
<strong>سقف ارسال موارد قدیمی در هر اجرا، به‌ازای هر اسکنر.</strong>
<br>• فقط وقتی «اجازهٔ ارسال موارد قدیمی» روشن باشد اثر دارد.
<br>• صفر = هیچ مورد قدیمی ارسال نمی‌شود (حتی با روشن‌بودن کلید).
<br>• این سقف <strong>در کنار</strong> «حداکثر پیامک هر اسکنر» اعمال می‌شود؛ پس
ممکن است ابتدا سقف کل (max_per_run) پر شود.
<br>• برای پاک‌کردن تدریجی انبار قدیمی، عدد کوچکی مثل ۵۰ بگذارید تا در هر اجرا
دسته‌ای ارسال شود.
</p>
</div>
</div>