estelam ticket, varizha , others

This commit is contained in:
javad committed 2026-10-13 15:48:24 +03:30
1 parent 35a1ec1794
commit 6b546aad76
71 files changed
+2865 -191

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
<svg class="dispatch-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
{% if icon == 'send' %}<path d="m22 2-7 20-4-9-9-4Z M22 2 11 13"/>
{% elif icon == 'shield' %}<path d="M12 3 4 6v6c0 5 8 9 8 9s8-4 8-9V6Z m-4 9 3 3 5-6"/>
{% elif icon == 'history' %}<path d="M3 11a9 9 0 1 1 3 8 M3 4v7h7 M12 7v5l3 2"/>
{% elif icon == 'people' %}<circle cx="9" cy="7" r="3"/><path d="M3 21v-3a6 6 0 0 1 12 0v3 M17 4a3 3 0 0 1 0 6 M18 15a5 5 0 0 1 3 4v2"/>
{% elif icon == 'check' %}<path d="m5 12 4 4L19 6"/>
{% elif icon == 'eye' %}<path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/>
{% elif icon == 'reply' %}<path d="m9 6-6 6 6 6 M3 12h10a7 7 0 0 1 7 7"/>
{% elif icon == 'attachment' %}<path d="m8 12 6-6a3 3 0 0 1 4 4L9 19a5 5 0 0 1-7-7L12 2"/>
{% elif icon == 'arrow' %}<path d="M20 12H4 m6-6-6 6 6 6"/>
{% else %}<circle cx="12" cy="12" r="9"/><path d="M12 7v6 M12 17h.01"/>{% endif %}
</svg>

After

Width:  |  Height:  |  Size: 1020 B

@@ -4,6 +4,7 @@
<div class="staff-ticket-main">
<div class="staff-ticket-head"><span class="ticket-number" dir="ltr">{{ ticket.ticket_number }}</span>{% if ticket.status == 'WAITING_STAFF' or ticket.status == 'REOPENED' %}<span class="action-required">نیازمند پاسخ</span>{% endif %}<time>{{ ticket.last_activity_at|jalali_datetime }}</time></div>
<h2>{{ ticket.subject }}</h2>
{% if ticket.group_dispatch %}<p class="ticket-origin-label">ایجادشده از {{ ticket.group_dispatch.get_operation_type_display }}: {{ ticket.group_dispatch.subject }}</p>{% endif %}
{% if not ticket.limited_share and ticket.owner and ticket.owner != user and not perms.tickets.view_all_tickets %}
<p class="ticket-locked">قفل‌شده توسط {{ ticket.owner.staff_profile.display_name }} · جزئیات پس از انتقال قابل مشاهده است</p>
{% else %}
+1 -1
View File
@@ -6,7 +6,7 @@
<header class="ticket-workspace-head"><div class="ticket-title"><a class="workspace-back" href="{% if magic_link_access %}{% url 'accounts:login' %}{% else %}{% url 'tickets:list' %}{% endif %}" aria-label="بازگشت">→</a><div><span class="ticket-number" dir="ltr">{{ ticket.ticket_number }}</span><h1>{{ ticket.subject }}</h1></div></div><div class="ticket-head-actions"><span class="badge" data-ticket-status>{{ ticket.get_status_display }}</span><button type="button" class="icon-button" data-toggle-panel="ticket-info" aria-label="جزئیات تیکت">ⓘ</button></div></header>
<div class="workspace-grid customer-grid">
<main class="conversation-pane">
<section class="conversation-toolbar"><div><b>سوابق مکاتبات تیکت</b></div><small><span data-conversation-count>{{ conversation|length }}</span> مکاتبه</small></section>
<section class="conversation-toolbar"><div><b>{% if ticket.group_dispatch %}{{ ticket.group_dispatch.get_operation_type_display }}{% else %}سوابق مکاتبات تیکت{% endif %}</b>{% if ticket.group_dispatch.response_control.mode == 'SEALED' and not ticket.group_dispatch.response_control.opened_at %}<small role="status">پاسخ شما تا بازگشایی برای کارکنان محرمانه است.</small>{% endif %}</div><small><span data-conversation-count>{{ conversation|length }}</span> مکاتبه</small></section>
<section class="conversation" data-conversation role="log" aria-live="polite" aria-label="مکاتبات تیکت">{% if has_older %}<button class="load-older" type="button" data-load-older hx-get="{% url 'tickets:messages' ticket.public_id %}?before={{ older_cursor }}" hx-target="this" hx-swap="outerHTML">↑ نمایش مکاتبات قدیمی‌تر</button>{% endif %}{% include 'tickets/_messages.html' %}<div class="conversation-end" data-conversation-end aria-hidden="true"></div></section>
<button class="new-message-jump" type="button" data-jump-latest hidden>↓ رفتن به آخرین مکاتبه</button>
<section class="composer {% if request.GET.reply_result != 'error' %}is-collapsed{% endif %}" data-composer-shell>
@@ -0,0 +1,14 @@
{% extends 'base.html' %}{% load static %}
{% block title %}تأیید بازگشایی پاسخ‌ها{% endblock %}
{% block extra_head %}<link rel="stylesheet" href="{% static 'css/dispatch-workspace.css' %}?v={{ static_asset_version }}">{% endblock %}
{% block content %}
<main class="operations-page dispatch-workspace dispatch-open-confirm">
<section class="dispatch-response-banner"><h1>تأیید بازگشایی همه پاسخ‌ها</h1><h2>{{ dispatch.subject }}</h2>
<p>{{ respondent_count }} مخاطب پاسخ داده‌اند. با تأیید، پاسخ‌های این ارسال هم‌زمان برای کاربران مجاز قابل مشاهده می‌شوند. این عملیات یک‌بار انجام می‌شود.</p>
{% if control.notify_sms %}<p>پیامک زیر برای پاسخ‌دهندگان موجود در لحظه بازگشایی در صف ارسال قرار می‌گیرد:</p><blockquote class="opening-sms-preview">{{ opening_sms|linebreaksbr }}</blockquote>
{% else %}<p>اطلاع‌رسانی پیامکی این ارسال غیرفعال است؛ پیامکی ارسال نمی‌شود.</p>{% endif %}
<p><strong>آیا اطمینان دارید که می‌خواهید همه پاسخ‌ها را بازگشایی کنید؟</strong></p>
<form method="post">{% csrf_token %}<input type="hidden" name="confirmation" value="{{ confirmation }}"><div class="dispatch-opening-actions"><a class="dispatch-secondary" href="{% url 'staff_tickets:dispatch_detail' dispatch.public_id %}">بازگشت بدون بازگشایی</a><button type="submit" class="dispatch-primary">تأیید و بازگشایی همه پاسخ‌ها</button></div></form>
</section>
</main>
{% endblock %}
+2 -2
View File
@@ -8,8 +8,8 @@
</header>
{% if ticket.group_dispatch %}
<section class="ticket-dispatch-context" aria-label="اطلاعات ارسال گروهی">
<div><span aria-hidden="true">⇶</span><p><small>ارسال گروهی</small><b>{{ ticket.group_dispatch.subject }}</b><em>{{ ticket.group_dispatch.contact_group.name }}</em></p></div>
<div class="ticket-dispatch-context-actions"><a href="{% url 'staff_tickets:dispatch_detail' ticket.group_dispatch.public_id %}">مشاهده گزارش گروه</a>{% if can_resolve_dispatch and group_open_count %}<form method="post" action="{% url 'staff_tickets:dispatch_resolve' ticket.group_dispatch.public_id %}" onsubmit="return confirm('هر {{ group_open_count }} تیکت باز این ارسال گروهی حل‌شده می‌شوند. آیا مطمئن هستید؟')">{% csrf_token %}<input type="hidden" name="return_ticket" value="{{ ticket.public_id }}"><button type="submit">✓ حل‌شدن تمامی تیکت‌های گروه ({{ group_open_count }})</button></form>{% endif %}</div>
<div><span aria-hidden="true">⇶</span><p><small>{{ ticket.group_dispatch.get_operation_type_display }}</small><b>{{ ticket.group_dispatch.subject }}</b><em>{{ ticket.group_dispatch.contact_group.name }}{% if ticket.group_dispatch.response_control.mode == 'SEALED' and not ticket.group_dispatch.response_control.opened_at %} — پاسخ‌ها تا بازگشایی محرمانه‌اند{% endif %}</em></p></div>
<div class="ticket-dispatch-context-actions">{% if can_view_dispatch %}<a href="{% url 'staff_tickets:dispatch_detail' ticket.group_dispatch.public_id %}">مشاهده گزارش گروه</a>{% endif %}{% if can_resolve_dispatch and group_open_count %}<form method="post" action="{% url 'staff_tickets:dispatch_resolve' ticket.group_dispatch.public_id %}" onsubmit="return confirm('هر {{ group_open_count }} تیکت باز این ارسال گروهی حل‌شده می‌شوند. آیا مطمئن هستید؟')">{% csrf_token %}<input type="hidden" name="return_ticket" value="{{ ticket.public_id }}"><button type="submit">✓ حل‌شدن تمامی تیکت‌های گروه ({{ group_open_count }})</button></form>{% endif %}</div>
<button type="button" class="ticket-dispatch-context-close" data-dismiss-dispatch-context aria-label="بستن پیام ارسال گروهی" title="بستن">×</button>
</section>
{% endif %}
+87 -16
View File
@@ -1,17 +1,88 @@
{% extends 'base.html' %}{% load static %}
{% extends 'base.html' %}{% load static jalali %}
{% block title %}{{ page_title }}{% endblock %}
{% block content %}<div class="operations-page operations-dispatch">
<section class="operations-hero"><div><p class="eyebrow">ارسال انبوه امن</p><h1>ارسال گروهی ایزوله</h1><p>برای هر مخاطب یک تیکت و Magic Link مستقل ایجاد می‌شود؛ هیچ دریافت‌کننده‌ای پاسخ دیگران را نمی‌بیند.</p></div><div class="security-callout"><span>⌾</span><div><b>حریم خصوصی فعال</b><small>تیکت مستقل برای هر گیرنده</small></div></div></section>
<section class="operations-metrics dispatch-metrics" aria-label="آمار ارسال‌های گروهی"><article><small>کل عملیات</small><b>{{ metrics.total }}</b></article><article><small>کل گیرندگان</small><b>{{ metrics.recipients }}</b></article><article><small>ارسال کامل</small><b>{{ metrics.sent }}</b></article><article><small>نیازمند پیگیری</small><b>{{ metrics.attention }}</b></article></section>
<div class="dispatch-layout"><form method="post" enctype="multipart/form-data" class="dispatch-composer" data-disable-on-submit data-ticket-composer>{% csrf_token %}{% if form.non_field_errors %}<div class="create-form-errors" role="alert">{{ form.non_field_errors }}</div>{% endif %}
<section><header><span>۱</span><div><h2>نوع عملیات، مخاطبان و مالکیت</h2><p>قابلیت‌های مجاز و گروه هدف را مشخص کنید.</p></div></header><div class="editor-grid"><label><span>{{ form.operation_type.label }}</span>{{ form.operation_type }}{{ form.operation_type.errors }}</label><label><span>{{ form.unit.label }}</span>{{ form.unit }}{{ form.unit.errors }}</label><label><span>{{ form.contact_group.label }}</span>{{ form.contact_group }}{{ form.contact_group.errors }}<small>فقط گروه‌های واحد انتخاب‌شده معتبر هستند.</small></label></div></section>
<section><header><span>۲</span><div><h2>موضوع و دسته‌بندی</h2><p>عنوان کوتاه و برچسب‌های مناسب، گزارش‌گیری را دقیق‌تر می‌کند.</p></div></header><div class="editor-grid"><label><span>{{ form.category.label }}</span>{{ form.category }}{{ form.category.errors }}</label><label><span>{{ form.subject.label }}</span>{{ form.subject }}{{ form.subject.errors }}</label><label class="editor-wide"><span>{{ form.tags.label }}</span>{{ form.tags }}{{ form.tags.errors }}</label></div></section>
<section><header><span>۳</span><div><h2>متن و اسناد پیام</h2><p>درخواست را شفاف بنویسید و اسناد لازم را برای همه تیکت‌های خصوصی ضمیمه کنید.</p></div></header><label class="message-editor"><span>{{ form.body.label }}</span>{{ form.body }}{{ form.body.errors }}</label><div class="dispatch-attachment-picker media-picker"><div class="attachment-picker-copy"><span class="attachment-icon" aria-hidden="true">+</span><div><b>{{ form.files.label }}</b><small>{{ form.files.help_text }}</small></div></div><div class="media-picker-actions"><label class="media-picker-button gallery-button" for="dispatch-gallery-input">▣ افزودن عکس از گالری</label><label class="media-picker-button" for="dispatch-file-input">+ افزودن فایل</label></div><input id="dispatch-gallery-input" type="file" accept="image/*" multiple data-gallery-input><input id="dispatch-file-input" type="file" multiple data-document-input><span class="canonical-files">{{ form.files }}</span>{{ form.files.errors }}<small class="compression-note" data-compression-status>تصاویر بزرگ پیش از ارسال روی دستگاه شما بهینه می‌شوند؛ هر پیوست را می‌توانید پیش از ثبت حذف کنید.</small><div class="file-preview" data-file-preview aria-live="polite" aria-label="پیش‌نمایش پیوست‌های انتخاب‌شده"></div></div></section>
<footer><div><b>قبل از ارسال بررسی کنید</b><small>این عملیات برای همه اعضای گروه تیکت مستقل ایجاد می‌کند.</small></div><button type="submit">ایجاد تیکت‌ها و قرار دادن پیامک در صف ←</button></footer>
</form><aside class="dispatch-history"><header><div><small>ردیابی عملیات</small><h2>ارسال‌های اخیر</h2></div><span>{{ page.paginator.count }}</span></header>
<form method="get" class="dispatch-filter" aria-label="فیلتر ارسال‌ها"><label class="dispatch-search"><span>جستجو</span><input type="search" name="q" value="{{ request.GET.q }}" placeholder="موضوع، گروه یا کارشناس"></label><div><label><span>وضعیت</span><select name="status"><option value="">همه وضعیت‌ها</option>{% for value,label in dispatch_statuses %}<option value="{{ value }}" {% if request.GET.status == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><label><span>واحد</span><select name="unit"><option value="">همه واحدها</option>{% for item in units %}<option value="{{ item.pk }}" {% if request.GET.unit == item.pk|stringformat:'s' %}selected{% endif %}>{{ item.name }}</option>{% endfor %}</select></label></div><label><span>گروه مخاطب</span><select name="group"><option value="">همه گروه‌ها</option>{% for item in groups %}<option value="{{ item.pk }}" {% if request.GET.group == item.pk|stringformat:'s' %}selected{% endif %}>{{ item.unit.name }} — {{ item.name }}</option>{% endfor %}</select></label><div><label><span>از تاریخ</span><input type="date" name="created_from" value="{{ request.GET.created_from }}"></label><label><span>تا تاریخ</span><input type="date" name="created_to" value="{{ request.GET.created_to }}"></label></div><footer><button type="submit">اعمال فیلتر</button><a href="{% url 'staff_tickets:dispatch_create' %}">پاک کردن</a></footer></form>
<div class="dispatch-results">{% for item in dispatches %}<a class="dispatch-history-card" href="{% url 'staff_tickets:dispatch_detail' item.public_id %}"><span class="dispatch-state state-{{ item.status|lower }}"></span><div><b>{{ item.subject }}</b><small>{{ item.get_operation_type_display }} · {{ item.unit }} · {{ item.contact_group.name }}</small><p><span>{{ item.get_status_display }}</span><span>{{ item.recipient_count }} گیرنده</span><span>{{ item.created_at|date:'Y/m/d H:i' }}</span></p></div><em>←</em></a>{% empty %}<div class="empty-state"><span>⌕</span><p>ارسالی مطابق فیلترهای انتخابی پیدا نشد.</p></div>{% endfor %}</div>
{% if page.paginator.num_pages > 1 %}<nav class="operations-pagination" aria-label="صفحه‌بندی ارسال‌ها">{% if page.has_previous %}<a href="?q={{ request.GET.q|urlencode }}&status={{ request.GET.status|urlencode }}&unit={{ request.GET.unit|urlencode }}&group={{ request.GET.group|urlencode }}&created_from={{ request.GET.created_from|urlencode }}&created_to={{ request.GET.created_to|urlencode }}&page={{ page.previous_page_number }}">قبلی</a>{% endif %}<span>صفحه {{ page.number }} از {{ page.paginator.num_pages }}</span>{% if page.has_next %}<a href="?q={{ request.GET.q|urlencode }}&status={{ request.GET.status|urlencode }}&unit={{ request.GET.unit|urlencode }}&group={{ request.GET.group|urlencode }}&created_from={{ request.GET.created_from|urlencode }}&created_to={{ request.GET.created_to|urlencode }}&page={{ page.next_page_number }}">بعدی</a>{% endif %}</nav>{% endif %}
</aside></div>
</div>{% endblock %}
{% block extra_scripts %}<script defer src="{% static 'vendor/compressor.min.js' %}?v={{ static_asset_version }}"></script><script defer src="{% static 'js/attachment-uploader.js' %}?v={{ static_asset_version }}"></script>{% endblock %}
{% block extra_head %}<link rel="stylesheet" href="{% static 'css/dispatch-workspace.css' %}?v={{ static_asset_version }}">{% endblock %}
{% block content %}
<div class="operations-page operations-dispatch dispatch-workspace" {% if can_create_dispatch %}data-dispatch-workspace{% endif %}>
<header class="dispatch-page-heading">
<div><p class="dispatch-eyebrow">ارتباط با گروه‌های مخاطبان</p><h1>ارسال گروهی ایزوله</h1><p>{% if can_create_dispatch %}یک پیام مشترک بفرستید؛ پاسخ هر مخاطب را در تیکت خصوصی خودش پیگیری کنید.{% else %}سوابق ارسال‌های مجاز و وضعیت پاسخ مخاطبان را پیگیری کنید.{% endif %}</p></div>
<span class="dispatch-privacy">{% include 'tickets/_dispatch_icon.html' with icon='shield' %} پاسخ‌ها برای سایر گیرندگان نمایش داده نمی‌شوند</span>
</header>
<section class="dispatch-summary-strip" aria-label="آمار ارسال‌های گروهی">
<div><span>کل ارسال‌ها</span><strong>{{ metrics.total }}</strong></div><div><span>کل گیرندگان</span><strong>{{ metrics.recipients }}</strong></div><div><span>ارسال به درگاه</span><strong>{{ metrics.sent }}</strong></div><div><span>در صف یا نیازمند پیگیری</span><strong>{{ metrics.attention }}</strong></div>
</section>
<nav class="dispatch-view-switch" aria-label="بخش‌های ارسال گروهی">
{% if can_create_dispatch %}<a id="dispatch-compose-tab" href="#dispatch-compose-panel" data-dispatch-tab="compose">{% include 'tickets/_dispatch_icon.html' with icon='send' %} ارسال جدید</a>{% endif %}
<a id="dispatch-history-tab" href="#dispatch-history-panel" data-dispatch-tab="history">{% include 'tickets/_dispatch_icon.html' with icon='history' %} سوابق ارسال <span>{{ page.paginator.count }}</span></a>
</nav>
{% if can_create_dispatch %}<section id="dispatch-compose-panel" data-dispatch-panel="compose" aria-labelledby="dispatch-compose-tab">
<div class="dispatch-compose-layout">
<form id="dispatch-compose-form" method="post" enctype="multipart/form-data" class="dispatch-composer" data-disable-on-submit data-ticket-composer>
{% csrf_token %}
{% if form.errors %}<div class="dispatch-error-summary" role="alert" tabindex="-1"><b>ارسال انجام نشد؛ موارد زیر را بررسی کنید.</b>{{ form.non_field_errors }}<ul>{% for field in form %}{% for error in field.errors %}<li><a href="#{{ field.id_for_label }}">{{ field.label }}: {{ error }}</a></li>{% endfor %}{% endfor %}</ul>{% if request.FILES %}<p>پیوست‌ها را دوباره انتخاب کنید.</p>{% endif %}</div>{% endif %}
<section><header><span>۱</span><div><h2>گیرندگان پیام</h2><p>واحد و گروه هدف را انتخاب کنید.</p></div></header><div class="editor-grid">
<label for="{{ form.operation_type.id_for_label }}"><span>{{ form.operation_type.label }}</span>{{ form.operation_type }}{{ form.operation_type.errors }}</label>
<label for="{{ form.unit.id_for_label }}"><span>{{ form.unit.label }} <em>الزامی</em></span>{{ form.unit }}{{ form.unit.errors }}</label>
<label class="editor-wide" for="{{ form.contact_group.id_for_label }}"><span>{{ form.contact_group.label }} <em>الزامی</em></span>{{ form.contact_group }}{{ form.contact_group.errors }}<small data-dispatch-group-hint aria-live="polite">فقط اعضای فعال دارای شماره همراه، پیام دریافت می‌کنند.</small></label>
</div></section>
<section><header><span>۲</span><div><h2>پیام شما</h2><p>موضوع روشن و متن مشترک برای تمام گیرندگان.</p></div></header><div class="editor-grid">
<label class="editor-wide" for="{{ form.subject.id_for_label }}"><span>{{ form.subject.label }} <em>الزامی</em></span>{{ form.subject }}{{ form.subject.errors }}</label>
<label class="editor-wide" for="{{ form.category.id_for_label }}"><span>{{ form.category.label }} <em>الزامی</em></span>{{ form.category }}{{ form.category.errors }}</label>
</div>
<label class="message-editor" for="{{ form.body.id_for_label }}"><span>{{ form.body.label }} <em>الزامی</em></span>{{ form.body }}{{ form.body.errors }}<small>این متن برای همه مشترک است؛ پاسخ‌ها در گفت‌وگوهای مستقل ثبت می‌شوند.</small></label>
<details class="dispatch-tags" {% if form.tags.value %}open{% endif %}><summary>برچسب‌های گزارش‌گیری <span>اختیاری</span></summary><label for="{{ form.tags.id_for_label }}">{{ form.tags.label }}{{ form.tags }}{{ form.tags.errors }}<small>برای انتخاب چند برچسب در رایانه، Ctrl یا Command را نگه دارید.</small></label></details>
<div class="dispatch-attachment-picker media-picker">
<div class="attachment-picker-copy">{% include 'tickets/_dispatch_icon.html' with icon='attachment' %}<div><b>{{ form.files.label }} <small>اختیاری</small></b><small>هر پیوست به تیکت خصوصی تمام گیرندگان اضافه می‌شود.</small></div></div>
<div class="media-picker-actions"><label class="media-picker-button gallery-button" for="dispatch-gallery-input">افزودن عکس از گالری</label><label class="media-picker-button" for="dispatch-file-input">افزودن فایل</label></div>
<input id="dispatch-gallery-input" type="file" accept="image/*" multiple data-gallery-input><input id="dispatch-file-input" type="file" multiple data-document-input><span class="canonical-files">{{ form.files }}</span>{{ form.files.errors }}
<small class="compression-note" data-compression-status>انتخاب فایل به معنی ارسال نیست؛ پیوست‌ها را پیش از ثبت می‌توانید حذف کنید.</small><div class="file-preview" data-file-preview aria-live="polite" aria-label="پیش‌نمایش پیوست‌های انتخاب‌شده"></div>
</div>
</section>
<section class="dispatch-response-options"><header><span>۳</span><div><h2>دسترسی به پاسخ‌ها</h2><p>روش مشاهده پاسخ‌ها پس از ایجاد ارسال قابل تغییر نیست.</p></div></header>
<fieldset><legend>{{ form.response_mode.label }}</legend>{{ form.response_mode }}{{ form.response_mode.errors }}</fieldset>
<p>در حالت یکجا، متن و پیوست پاسخ‌ها برای همه کارکنان، حتی admin، تا بازگشایی قفل می‌ماند. هر مخاطب پاسخ خودش را می‌بیند.</p>
<label class="dispatch-sms-choice">{{ form.notify_opening_sms }} <span>{{ form.notify_opening_sms.label }}</span></label>
<small data-opening-sms-hint aria-live="polite">فقط در حالت یکجا و برای حداکثر ۱۵ گیرندهٔ واجد شرایط مجاز است.</small>
</section>
<footer><div><b>ابتدا بررسی، سپس ارسال</b><small>قبل از تأیید نهایی، پیامک یا تیکتی ایجاد نمی‌شود.</small></div><button type="submit" data-dispatch-send>ایجاد تیکت‌ها و قرار دادن پیامک در صف</button></footer>
</form>
<aside class="dispatch-compose-guide" aria-label="خلاصه ارسال جدید">
<div class="dispatch-guide-icon">{% include 'tickets/_dispatch_icon.html' with icon='people' %}</div><h2>پیش از ارسال</h2><p>گیرندگان و پیام را بررسی کنید؛ هر فرد یک مسیر اختصاصی برای پاسخ خواهد داشت.</p>
<dl><div><dt>واحد ارسال‌کننده</dt><dd data-dispatch-unit-summary>انتخاب نشده</dd></div><div><dt>گروه مخاطبان</dt><dd data-dispatch-group-summary>انتخاب نشده</dd></div><div><dt>گیرندگان واجد شرایط</dt><dd data-dispatch-count-summary>پس از انتخاب گروه</dd></div></dl>
<p class="dispatch-guide-note">{% include 'tickets/_dispatch_icon.html' with icon='shield' %} اطلاعات سایر اعضا در تیکت مخاطب نمایش داده نمی‌شود.</p>
</aside>
</div>
</section>
{% endif %}<section id="dispatch-history-panel" class="dispatch-history" data-dispatch-panel="history" aria-labelledby="dispatch-history-tab">
<header><div><h2>سوابق و پیگیری ارسال‌ها</h2><p>هر ارسال را باز کنید تا وضعیت پیامک‌ها و پاسخ مخاطبان را ببینید.</p></div><span>{{ page.paginator.count }} ارسال</span></header>
<form method="get" class="dispatch-filter" aria-label="فیلتر ارسال‌ها">
<label class="dispatch-search"><span>جستجو</span><input type="search" name="q" value="{{ request.GET.q }}" placeholder="موضوع، گروه یا نام کاربری کارشناس"></label>
<label><span>وضعیت ارسال</span><select name="status"><option value="">همه وضعیت‌ها</option>{% for value,label in dispatch_statuses %}<option value="{{ value }}" {% if request.GET.status == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label>
<details class="dispatch-more-filters" {% if request.GET.unit or request.GET.group or request.GET.created_from or request.GET.created_to %}open{% endif %}><summary>فیلترهای بیشتر</summary><div>
<label><span>واحد</span><select name="unit"><option value="">همه واحدها</option>{% for item in history_units %}<option value="{{ item.pk }}" {% if request.GET.unit == item.pk|stringformat:'s' %}selected{% endif %}>{{ item.name }}</option>{% endfor %}</select></label>
<label><span>گروه مخاطب</span><select name="group"><option value="">همه گروه‌ها</option>{% for item in history_groups %}<option value="{{ item.pk }}" {% if request.GET.group == item.pk|stringformat:'s' %}selected{% endif %}>{{ item.unit.name }} — {{ item.name }}</option>{% endfor %}</select></label>
<label><span>از تاریخ (میلادی)</span><input type="date" name="created_from" value="{{ request.GET.created_from }}"></label><label><span>تا تاریخ (میلادی)</span><input type="date" name="created_to" value="{{ request.GET.created_to }}"></label>
</div></details>
<footer><button type="submit">اعمال فیلتر</button><a href="{% url 'staff_tickets:dispatch_create' %}#dispatch-history-panel">پاک کردن فیلترها</a></footer>
</form>
<div class="dispatch-results">{% for item in dispatches %}
<a class="dispatch-history-card" href="{% url 'staff_tickets:dispatch_detail' item.public_id %}">
<div class="dispatch-history-main"><b>{{ item.subject }}</b><small>{{ item.get_operation_type_display }} · {{ item.unit }} · {{ item.contact_group.name }}</small></div>
<span class="report-status status-{{ item.status|lower }}">{{ item.get_status_display }}</span><span class="dispatch-history-count">{{ item.recipient_count }} گیرنده</span>
<time datetime="{{ item.created_at|date:'c' }}">{{ item.created_at|jalali_datetime }}</time><span class="dispatch-history-open">مشاهده گزارش {% include 'tickets/_dispatch_icon.html' with icon='arrow' %}</span>
</a>
{% empty %}<div class="dispatch-empty">{% include 'tickets/_dispatch_icon.html' with icon='history' %}<h3>{% if metrics.total %}ارسالی با این فیلترها پیدا نشد{% else %}هنوز ارسال گروهی ثبت نشده است{% endif %}</h3><p>{% if metrics.total %}عبارت جستجو یا فیلترها را تغییر دهید.{% else %}از بخش «ارسال جدید» اولین پیام گروهی را آماده کنید.{% endif %}</p></div>{% endfor %}</div>
{% if page.paginator.num_pages > 1 %}<nav class="operations-pagination" aria-label="صفحه‌بندی ارسال‌ها">{% if page.has_previous %}<a href="?q={{ request.GET.q|urlencode }}&status={{ request.GET.status|urlencode }}&unit={{ request.GET.unit|urlencode }}&group={{ request.GET.group|urlencode }}&created_from={{ request.GET.created_from|urlencode }}&created_to={{ request.GET.created_to|urlencode }}&page={{ page.previous_page_number }}#dispatch-history-panel">قبلی</a>{% endif %}<span>صفحه {{ page.number }} از {{ page.paginator.num_pages }}</span>{% if page.has_next %}<a href="?q={{ request.GET.q|urlencode }}&status={{ request.GET.status|urlencode }}&unit={{ request.GET.unit|urlencode }}&group={{ request.GET.group|urlencode }}&created_from={{ request.GET.created_from|urlencode }}&created_to={{ request.GET.created_to|urlencode }}&page={{ page.next_page_number }}#dispatch-history-panel">بعدی</a>{% endif %}</nav>{% endif %}
</section>
<dialog class="dispatch-review-dialog" aria-labelledby="dispatch-review-title" data-dispatch-review>
<header><div><p class="dispatch-eyebrow">هنوز ارسال نشده است</p><h2 id="dispatch-review-title">بررسی نهایی ارسال گروهی</h2></div><button type="button" class="dispatch-dialog-close" aria-label="بستن پیش‌نمایش" data-dispatch-review-close>×</button></header>
<dl class="dispatch-review-meta"><div><dt>واحد</dt><dd data-review-unit></dd></div><div><dt>گروه</dt><dd data-review-group></dd></div><div><dt>تعداد گیرندگان</dt><dd data-review-count></dd></div><div><dt>پیوست‌ها</dt><dd data-review-files></dd></div></dl>
<p data-review-response-policy></p>
<aside class="dispatch-inquiry-recommendation" data-review-inquiry-recommendation hidden role="note"><strong>توصیه برای استعلام‌گیری</strong><p>برای تمامی استعلام‌گیری‌ها توصیه می‌کنیم «بازگشایی یکجا» و «اطلاع‌رسانی بازگشایی با پیامک» را فعال کنید؛ این دو گزینه به شفافیت و اعتماد مخاطبان کمک می‌کنند.</p><p data-review-inquiry-limit hidden>در گروه‌های بیش از ۱۵ گیرنده، پیامک بازگشایی مجاز نیست؛ بازگشایی یکجا را فعال نگه دارید.</p></aside>
<h3 data-review-subject></h3><p class="dispatch-review-body" data-review-body></p>
<p class="dispatch-review-warning">با تأیید، برای هر گیرنده یک تیکت خصوصی ایجاد و پیامک در صف ارسال قرار می‌گیرد. تعداد نهایی بر اساس اعضای واجد شرایط در لحظه ثبت محاسبه می‌شود.</p>
<footer><button type="button" class="dispatch-secondary" data-dispatch-review-close>بازگشت و ویرایش</button><button type="button" class="dispatch-primary" data-dispatch-confirm>تأیید و ارسال گروهی</button></footer>
</dialog>
{{ dispatch_options|json_script:'dispatch-options' }}
</div>
{% endblock %}
{% block extra_scripts %}<script defer src="{% static 'vendor/compressor.min.js' %}?v={{ static_asset_version }}"></script><script defer src="{% static 'js/attachment-uploader.js' %}?v={{ static_asset_version }}"></script><script defer src="{% static 'js/dispatch-workspace.js' %}?v={{ static_asset_version }}"></script>{% endblock %}
+70 -70
View File
@@ -1,83 +1,83 @@
{% extends 'base.html' %}{% load jalali %}
{% extends 'base.html' %}{% load static jalali %}
{% block title %}{{ dispatch.subject }}{% endblock %}
{% block extra_head %}<link rel="stylesheet" href="{% static 'css/dispatch-workspace.css' %}?v={{ static_asset_version }}">{% endblock %}
{% block content %}
<div class="operations-page operations-dispatch-detail">
<section class="dispatch-report-hero">
<div class="dispatch-report-title">
<div class="dispatch-title-line"><span class="report-status status-{{ dispatch.status|lower }}">{{ dispatch.get_status_display }}</span><span class="eyebrow">گزارش ارسال گروهی</span></div>
<h1>{{ dispatch.subject }}</h1>
<div class="dispatch-meta-list">
<span><b>واحد</b>{{ dispatch.unit }}</span>
<span><b>گروه مخاطبان</b>{{ dispatch.contact_group.name }}</span>
<span><b>تعداد گیرندگان</b>{{ dispatch.recipient_count }} نفر</span>
<span><b>زمان ایجاد</b><time datetime="{{ dispatch.created_at|date:'c' }}">{{ dispatch.created_at|jalali_datetime }}</time></span>
</div>
</div>
<div class="dispatch-head-actions">
<a class="button" href="{% url 'staff_tickets:dispatch_create' %}">+ ارسال گروهی جدید</a>
{% if metrics.failed %}<form method="post" action="{% url 'staff_tickets:dispatch_retry' dispatch.public_id %}">{% csrf_token %}<button class="button button-danger-soft" type="submit">↻ تلاش مجدد {{ metrics.failed }} پیامک</button></form>{% endif %}
{% if can_resolve_dispatch and open_ticket_count %}<form method="post" action="{% url 'staff_tickets:dispatch_resolve' dispatch.public_id %}" onsubmit="return confirm('با تأیید این عملیات، هر {{ open_ticket_count }} تیکت باز این ارسال گروهی یک‌جا حل‌شده می‌شوند. آیا ادامه می‌دهید؟')">{% csrf_token %}<button class="button dispatch-resolve-button" type="submit">✓ حل‌شدن همه تیکت‌ها ({{ open_ticket_count }})</button></form>{% endif %}
</div>
</section>
{% if can_resolve_dispatch %}<aside class="dispatch-resolution-warning" role="note"><span aria-hidden="true">!</span><div><b>{% if open_ticket_count %}پایان رسیدگی گروهی{% else %}رسیدگی این گروه پایان یافته است{% endif %}</b><p>{% if open_ticket_count %}دکمه «حل‌شدن همه تیکت‌ها» وضعیت {{ open_ticket_count }} تیکت باز این عملیات را هم‌زمان تغییر می‌دهد. پاسخ تازه مشتری می‌تواند تیکت مربوط به خودش را دوباره باز کند.{% else %}همه تیکت‌های این عملیات اکنون حل یا بسته هستند.{% endif %}</p></div></aside>{% endif %}
<section class="dispatch-original-message" aria-labelledby="dispatch-message-title">
<header><div><span class="panel-index">متن اولیه کارشناس</span><h2 id="dispatch-message-title">پیام اصلی این ارسال</h2></div><span>شناسه عملیات: <b dir="ltr">{{ dispatch.public_id }}</b></span></header>
<blockquote>{{ dispatch.body|linebreaksbr }}</blockquote>
<footer><span>ایجادکننده: <b>{{ dispatch.created_by.staff_profile.display_name|default:dispatch.created_by.username }}</b></span><span>این متن در یک تیکت مستقل برای هر مخاطب ثبت شده است.</span></footer>
</section>
<section class="delivery-overview" aria-label="خلاصه دلیوری">
<div class="delivery-progress-copy"><div><span class="panel-index">پیشرفت تحویل</span><strong>{{ metrics.delivery_percent }}٪</strong></div><p>{{ metrics.delivered }} از {{ dispatch.recipient_count }} پیامک تحویل قطعی شده است.</p></div>
<div class="delivery-progress-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ metrics.delivery_percent }}"><span style="width:{{ metrics.delivery_percent }}%"></span></div>
<div class="delivery-overview-note">{% if metrics.tracking %}<b>{{ metrics.tracking }} پیامک در حال پیگیری است</b><span>نتیجه‌های موقت هر ۳ دقیقه تا ۴۵ دقیقه دوباره استعلام می‌شوند.</span>{% elif metrics.failed %}<b>موارد ناموفق نیاز به بررسی دارند</b><span>علت دقیق و دکمه تلاش مجدد در ردیف مخاطب قرار دارد.</span>{% else %}<b>پردازش صف پایان یافته است</b><span>برای جزئیات هر مخاطب، گزارش پایین صفحه را ببینید.</span>{% endif %}</div>
</section>
<div class="operations-page operations-dispatch-detail dispatch-workspace" data-dispatch-report>
<nav class="dispatch-breadcrumb" aria-label="مسیر صفحه"><a href="{% url 'staff_tickets:dispatch_create' %}#dispatch-history-panel">{% include 'tickets/_dispatch_icon.html' with icon='arrow' %} سوابق ارسال گروهی</a><span>گزارش و پاسخ‌ها</span></nav>
<header class="dispatch-report-hero">
<div class="dispatch-report-title"><div class="dispatch-title-line"><span class="dispatch-eyebrow">{{ dispatch.get_operation_type_display }}</span><span class="report-status status-{{ dispatch.status|lower }}">{{ dispatch.get_status_display }}</span></div><h1>{{ dispatch.subject }}</h1>
<div class="dispatch-meta-list"><span><b>واحد</b>{{ dispatch.unit }}</span><span><b>گروه</b>{{ dispatch.contact_group.name }}</span><span><b>گیرندگان</b>{{ dispatch.recipient_count }} نفر</span><span><b>ایجاد</b><time datetime="{{ dispatch.created_at|date:'c' }}">{{ dispatch.created_at|jalali_datetime }}</time></span></div>
</div><a class="dispatch-secondary" href="{% url 'staff_tickets:dispatch_create' %}">{% include 'tickets/_dispatch_icon.html' with icon='send' %} ارسال گروهی جدید</a>
</header>
{% if response_control %}<section class="dispatch-response-banner" aria-label="وضعیت بازگشایی پاسخ‌ها">
{% if responses_locked %}<h2>پاسخ‌ها هنوز بازگشایی نشده‌اند</h2><p>{{ metrics.replied }} مخاطب پاسخ داده‌اند. متن و پیوست پاسخ‌ها برای همه کارکنان، حتی admin، تا بازگشایی قفل است.</p>
{% if can_open_responses %}<a class="dispatch-primary" href="{% url 'staff_tickets:dispatch_open_responses' dispatch.public_id %}">بازگشایی همه پاسخ‌ها</a>{% else %}<p>بازگشایی توسط ایجادکننده یا کاربر دارای مجوز انجام می‌شود.</p>{% endif %}
{% elif response_control.opened_at %}<h2>پاسخ‌ها بازگشایی شده‌اند</h2><p>{{ response_control.opened_at|jalali_datetime_precise }} — توسط {{ response_control.opened_by.get_full_name|default:response_control.opened_by.username }}</p><p>پاسخ‌های جدید و ویرایش‌های بعدی برای کاربران مجاز قابل مشاهده‌اند.</p>
{% for item in opening_jobs %}<p>پیامک بازگشایی: {{ item.count }} مورد — {% if item.status == 'PENDING' %}در صف{% elif item.status == 'SENT' %}ارسال‌شده{% elif item.status == 'FAILED' %}ناموفق{% elif item.status == 'RETRYING' %}در انتظار تلاش مجدد{% elif item.status == 'PROCESSING' %}در حال ارسال{% else %}لغوشده{% endif %}</p>{% endfor %}
{% else %}<h2>مشاهده جداگانه پاسخ‌ها</h2><p>هر پاسخ پس از ثبت برای کاربران مجاز قابل مشاهده است. پیامک بازگشایی غیرفعال است.</p>{% endif %}
</section>{% endif %}
<section class="dispatch-kpi-grid" aria-label="شاخص‌های ارسال گروهی">
<article class="dispatch-kpi kpi-success"><span class="kpi-icon">✓</span><div><small>تحویل قطعی</small><strong>{{ metrics.delivered }}</strong><p>رسیده به گوشی</p></div></article>
<article class="dispatch-kpi kpi-info"><span class="kpi-icon">◌</span><div><small>پیگیری فعال</small><strong>{{ metrics.tracking }}</strong><p>{{ metrics.sent }} مورد پذیرفته‌شده توسط درگاه</p></div></article>
<article class="dispatch-kpi kpi-danger"><span class="kpi-icon">!</span><div><small>نیازمند اقدام</small><strong>{{ metrics.failed }}</strong><p>ناموفق یا تحویل‌نشده قطعی</p></div></article>
<article class="dispatch-kpi kpi-view"><span class="kpi-icon">◉</span><div><small>تعامل با لینک</small><strong>{{ metrics.opened }}</strong><p>{{ metrics.total_views }} بازدید · {{ metrics.not_opened }} مخاطب بدون بازدید</p></div></article>
<article class="dispatch-kpi kpi-reply"><span class="kpi-icon">↵</span><div><small>پاسخ دریافتی</small><strong>{{ metrics.replied }}</strong><p>پاسخ ثبت‌شده در تیکت‌ها</p></div></article>
<article class="dispatch-kpi kpi-success">{% include 'tickets/_dispatch_icon.html' with icon='check' %}<div><small>تحویل به گوشی</small><strong>{{ metrics.delivered }}</strong><p>تحویل قطعی پیامک</p></div></article>
<article class="dispatch-kpi kpi-info">{% include 'tickets/_dispatch_icon.html' with icon='history' %}<div><small>در صف ارسال</small><strong>{{ metrics.pending }}</strong><p>{{ metrics.tracking }} پیامک در پیگیری تحویل</p></div></article>
<article class="dispatch-kpi kpi-danger">{% include 'tickets/_dispatch_icon.html' %}<div><small>نیازمند اقدام</small><strong>{{ metrics.failed }}</strong><p>ناموفق یا تحویل تأییدنشده</p></div></article>
<article class="dispatch-kpi kpi-view">{% include 'tickets/_dispatch_icon.html' with icon='eye' %}<div><small>مخاطبان بازدیدکننده</small><strong>{{ metrics.opened }}</strong><p>{{ metrics.total_views }} بازدید از لینک‌ها</p></div></article>
<article class="dispatch-kpi kpi-reply">{% include 'tickets/_dispatch_icon.html' with icon='reply' %}<div><small>پاسخ دریافتی</small><strong>{{ metrics.replied }}</strong><p>مخاطب دارای پاسخ</p></div></article>
</section>
<section class="ticket-stream dispatch-recipient-report">
<header class="stream-head"><div><span class="panel-index">جزئیات هر گیرنده</span><h2>وضعیت پیامک، مشاهده و پاسخ</h2></div><span>{{ dispatch.recipient_count }} مخاطب</span></header>
<div class="table-scroll"><table class="dispatch-detail-table"><thead><tr><th>مخاطب</th><th>تیکت</th><th>وضعیت پیامک</th><th>وضعیت تیکت</th><th>مشاهده لینک</th><th>آخرین پاسخ</th></tr></thead><tbody>
<section class="delivery-overview" aria-label="خلاصه تحویل پیامک">
<div class="delivery-progress-copy"><div><span>تحویل قطعی پیامک</span><strong>{{ metrics.delivery_percent }}٪</strong></div><p>{{ metrics.delivered }} از {{ dispatch.recipient_count }} پیامک به گوشی رسیده است.</p></div>
<div class="delivery-progress-track" role="progressbar" aria-label="درصد تحویل قطعی پیامک" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ metrics.delivery_percent }}"><span style="width:{{ metrics.delivery_percent }}%"></span></div>
<div class="delivery-overview-note">{% if metrics.pending %}<b>{{ metrics.pending }} پیامک در صف ارسال است</b>{% elif metrics.tracking %}<b>{{ metrics.tracking }} پیامک در حال پیگیری تحویل است</b>{% elif metrics.failed %}<b>موارد ناموفق را بررسی کنید</b>{% else %}<b>وضعیت تحویل در گزارش هر مخاطب</b>{% endif %}<span>تحویل پیامک به معنی مشاهده لینک یا دریافت پاسخ نیست.</span></div>
</section>
<section class="dispatch-original-message" aria-labelledby="dispatch-message-title">
<header><div><span class="dispatch-eyebrow">متن اولیه کارشناس</span><h2 id="dispatch-message-title">پیام اصلی این ارسال</h2></div><span>ایجادکننده: <b>{{ dispatch.created_by.staff_profile.display_name|default:dispatch.created_by.username }}</b></span></header>
<blockquote>{{ dispatch.body|linebreaksbr }}</blockquote>
<details class="dispatch-operation-details"><summary>اطلاعات عملیات</summary><dl><div><dt>شناسه عملیات</dt><dd dir="ltr">{{ dispatch.public_id }}</dd></div><div><dt>دسته‌بندی</dt><dd>{{ dispatch.category }}</dd></div></dl></details>
</section>
<section class="dispatch-recipient-report" aria-labelledby="dispatch-recipients-title">
<header class="stream-head"><div><h2 id="dispatch-recipients-title">وضعیت پیامک، مشاهده و پاسخ</h2><p>پاسخ‌های هر مخاطب همراه با زمان ثبت و پیوست‌ها در همین گزارش نمایش داده می‌شوند.</p></div><span>{{ dispatch.recipient_count }} مخاطب</span></header>
<div class="dispatch-recipient-tools" hidden data-recipient-tools>
<label><span>جستجوی مخاطب</span><input type="search" data-recipient-search placeholder="نام، شماره همراه یا شماره تیکت"></label>
<label><span>نمایش مخاطبان</span><select data-recipient-filter><option value="all">همه مخاطبان</option><option value="unanswered">بدون پاسخ</option><option value="answered">دارای پاسخ</option><option value="unopened">بدون بازدید لینک</option><option value="opened">دارای بازدید لینک</option><option value="retryable">پیامک نیازمند اقدام</option><option value="active">تیکت‌های باز</option></select></label>
<button type="button" class="dispatch-secondary" data-recipient-reset>پاک کردن فیلتر</button><p role="status" data-recipient-result-count></p>
</div>
<div class="table-scroll"><table class="dispatch-detail-table"><caption class="dispatch-sr-only">گیرندگان این ارسال، وضعیت پیامک و تمامی پاسخ‌های هر مخاطب</caption><thead><tr><th scope="col">مخاطب و تیکت</th><th scope="col">وضعیت پیامک</th><th scope="col">مشاهده لینک</th><th scope="col">پاسخ‌ها و زمان ثبت</th></tr></thead><tbody>
{% for ticket in tickets %}
<tr>
<td data-label="مخاطب"><div class="recipient-cell"><span class="recipient-avatar">{{ ticket.customer.customer_profile.first_name|first|default:'•' }}</span><div><strong>{{ ticket.customer.customer_profile|default:ticket.customer.username }}</strong><small dir="ltr">{{ ticket.customer.local_mobile }}</small></div></div></td>
<td data-label="تیکت"><a class="ticket-reference" href="{% url 'staff_tickets:detail' ticket.public_id %}"><strong dir="ltr">{{ ticket.ticket_number }}</strong><span>مشاهده تیکت ←</span></a></td>
<tr data-recipient-row data-answered="{{ ticket.response_count|yesno:'true,false' }}" data-opened="{{ ticket.magic_total_views|yesno:'true,false' }}" data-retryable="{{ ticket.sms_retryable|yesno:'true,false' }}" data-active="{% if ticket.status == 'RESOLVED' or ticket.status == 'CLOSED' %}false{% else %}true{% endif %}" data-search="{{ ticket.customer.customer_profile|default:ticket.customer.username }} {{ ticket.customer.local_mobile }} {{ ticket.ticket_number }}">
<td data-label="مخاطب و تیکت"><div class="recipient-cell"><span class="recipient-avatar" aria-hidden="true">{{ ticket.customer.customer_profile.first_name|first|default:'•' }}</span><div><strong>{{ ticket.customer.customer_profile|default:ticket.customer.username }}</strong><small dir="ltr">{{ ticket.customer.local_mobile }}</small></div></div>{% if response_control.mode == 'SEALED' %}<span class="ticket-reference"><strong dir="ltr">{{ ticket.ticket_number }}</strong></span>{% else %}<a class="ticket-reference" href="{% url 'staff_tickets:detail' ticket.public_id %}"><strong dir="ltr">{{ ticket.ticket_number }}</strong></a>{% endif %}<span class="ticket-state-pill state-{{ ticket.status|lower }}">{{ ticket.get_status_display }}</span></td>
<td data-label="وضعیت پیامک" class="dispatch-sms-state">
<div class="sms-state-card tone-{{ ticket.sms_state_tone }}">
<span class="sms-state-icon">{% if ticket.sms_state_tone == 'success' %}✓{% elif ticket.sms_state_tone == 'danger' %}!{% elif ticket.sms_state_tone == 'warning' %}◌{% else %}↑{% endif %}</span>
<div><strong>{{ ticket.sms_state_title }}</strong><p>{{ ticket.sms_state_hint }}</p>{% if ticket.sms_job.delivery_next_check_at %}<small>استعلام بعدی: {{ ticket.sms_job.delivery_next_check_at|timeuntil }} دیگر</small>{% endif %}</div>
</div>
{% if ticket.sms_job %}
<details class="sms-technical-details">
<summary>جزئیات فنی و رهگیری</summary>
<dl>
<div><dt>ارسال</dt><dd>{% if ticket.sms_job.attempts == 1 %}موفق در اولین تلاش{% else %}{{ ticket.sms_job.attempts }} تلاش از {{ sms_max_attempts }}{% endif %}</dd></div>
{% if ticket.sms_job.provider_message_id %}<div><dt>شناسه رهگیری</dt><dd dir="ltr">{{ ticket.sms_job.provider_message_id }}</dd></div>{% endif %}
{% if ticket.sms_job.delivery_status_code %}<div><dt>کد دلیوری</dt><dd dir="ltr">{{ ticket.sms_job.delivery_status_code }}</dd></div>{% endif %}
{% if ticket.sms_job.delivery_check_attempts %}<div><dt>تعداد استعلام</dt><dd>{{ ticket.sms_job.delivery_check_attempts }} نوبت</dd></div>{% endif %}
{% if ticket.sms_job.delivery_checked_at %}<div><dt>آخرین استعلام</dt><dd>{{ ticket.sms_job.delivery_checked_at|jalali_datetime }}</dd></div>{% endif %}
</dl>
{% if ticket.sms_job.delivery_description %}<p class="gateway-technical-message">{{ ticket.sms_job.delivery_description }}</p>{% endif %}
</details>
{% if ticket.sms_job.last_error %}<div class="sms-error-panel"><b>علت دقیق عدم ارسال</b><p>{{ ticket.sms_job.last_error }}</p></div>{% endif %}
{% if ticket.sms_retryable %}<form method="post" action="{% url 'staff_tickets:dispatch_retry' dispatch.public_id %}" class="inline-retry-form">{% csrf_token %}<input type="hidden" name="job_id" value="{{ ticket.sms_job.pk }}"><button type="submit">↻ تلاش مجدد همین پیامک</button></form>{% endif %}
{% endif %}
<div class="sms-state-card tone-{{ ticket.sms_state_tone }}"><span class="sms-state-icon">{% if ticket.sms_state_tone == 'success' %}{% include 'tickets/_dispatch_icon.html' with icon='check' %}{% elif ticket.sms_state_tone == 'warning' %}{% include 'tickets/_dispatch_icon.html' with icon='history' %}{% else %}{% include 'tickets/_dispatch_icon.html' %}{% endif %}</span><div><strong>{{ ticket.sms_state_title }}</strong><p>{{ ticket.sms_state_hint }}</p>{% if ticket.sms_job.delivery_next_check_at %}<small>استعلام بعدی: {{ ticket.sms_job.delivery_next_check_at|timeuntil }} دیگر</small>{% endif %}</div></div>
{% if ticket.sms_job %}<details class="sms-technical-details"><summary>جزئیات فنی و رهگیری</summary><dl>
<div><dt>ارسال</dt><dd>{% if ticket.sms_job.status == 'SENT' and ticket.sms_job.attempts == 1 %}موفق در اولین تلاش{% else %}{{ ticket.sms_job.attempts }} تلاش از {{ sms_max_attempts }}{% endif %}</dd></div>
{% if ticket.sms_job.provider_message_id %}<div><dt>شناسه رهگیری</dt><dd dir="ltr">{{ ticket.sms_job.provider_message_id }}</dd></div>{% endif %}
{% if ticket.sms_job.delivery_status_code %}<div><dt>کد تحویل</dt><dd dir="ltr">{{ ticket.sms_job.delivery_status_code }}</dd></div>{% endif %}
{% if ticket.sms_job.delivery_check_attempts %}<div><dt>تعداد استعلام</dt><dd>{{ ticket.sms_job.delivery_check_attempts }} نوبت</dd></div>{% endif %}
{% if ticket.sms_job.delivery_checked_at %}<div><dt>آخرین استعلام</dt><dd>{{ ticket.sms_job.delivery_checked_at|jalali_datetime }}</dd></div>{% endif %}
</dl>{% if ticket.sms_job.delivery_description %}<p class="gateway-technical-message">{{ ticket.sms_job.delivery_description }}</p>{% endif %}</details>
{% if ticket.sms_job.last_error %}<div class="sms-error-panel"><b>علت عدم ارسال</b><p>{{ ticket.sms_job.last_error }}</p></div>{% endif %}
{% if ticket.sms_retryable %}<form method="post" action="{% url 'staff_tickets:dispatch_retry' dispatch.public_id %}" class="inline-retry-form">{% csrf_token %}<input type="hidden" name="job_id" value="{{ ticket.sms_job.pk }}"><button type="submit">تلاش مجدد همین پیامک</button></form>{% endif %}{% endif %}
</td>
<td data-label="وضعیت تیکت"><span class="ticket-state-pill state-{{ ticket.status|lower }}">{{ ticket.get_status_display }}</span></td>
<td data-label="مشاهده لینک">
<div class="engagement-state {% if ticket.magic_total_views %}has-engagement{% endif %}"><span>{% if ticket.magic_total_views %}◉{% else %}○{% endif %}</span><div>{% if ticket.magic_total_views %}<strong>{{ ticket.magic_total_views }} بازدید</strong>{% if ticket.magic_last_used_at %}<small>آخرین بازدید {{ ticket.magic_last_used_at|jalali_datetime }}</small>{% endif %}{% elif ticket.magic_link_count %}<strong>مشاهده نشده</strong><small>{{ ticket.magic_active_count }} لینک فعال</small>{% else %}<strong>لینک صادر نشده</strong>{% endif %}</div></div>
<td data-label="مشاهده لینک"><div class="engagement-state {% if ticket.magic_total_views %}has-engagement{% endif %}">{% include 'tickets/_dispatch_icon.html' with icon='eye' %}<div>{% if ticket.magic_total_views %}<strong>{{ ticket.magic_total_views }} بازدید</strong>{% if ticket.magic_last_used_at %}<small>آخرین بازدید {{ ticket.magic_last_used_at|jalali_datetime }}</small>{% endif %}{% elif ticket.magic_link_count %}<strong>مشاهده نشده</strong><small>{{ ticket.magic_active_count }} لینک فعال</small>{% else %}<strong>لینک صادر نشده</strong>{% endif %}</div></div></td>
<td data-label="پاسخ‌ها و زمان ثبت">
<p><b>پاسخ داده است: {{ ticket.response_count|yesno:'بله,خیر' }}</b></p>
{% if ticket.response_count %}<p>{{ ticket.response_count }} پاسخ</p><small>اولین پاسخ: {{ ticket.first_response_at|jalali_datetime_precise }}<br>آخرین پاسخ: {{ ticket.last_response_at|jalali_datetime_precise }}</small>
{% if responses_locked %}<p class="no-reply-state">متن و پیوست پاسخ تا بازگشایی محرمانه است.</p>
{% else %}{% for answer in ticket.report_customer_responses %}<article class="recipient-reply" aria-label="پاسخ {{ forloop.counter }}">
<time datetime="{{ answer.created_at|date:'c' }}">{{ answer.created_at|jalali_datetime_precise }}</time>
{% if answer.body %}<p>{{ answer.body|linebreaksbr }}</p>{% else %}<p>پاسخ دارای پیوست</p>{% endif %}
{% if answer.attachments.all %}<div class="dispatch-response-files">{% for file in answer.attachments.all %}<a href="{% url 'attachments:view' file.public_id %}" target="_blank" rel="noopener">{{ file.original_name }} · {{ file.file_size|filesizeformat }}<span class="dispatch-sr-only"> (باز کردن پیوست در پنجره جدید)</span></a>{% endfor %}</div>{% endif %}
</article>{% endfor %}{% endif %}
{% else %}<p class="no-reply-state">هنوز پاسخی ثبت نشده</p>{% endif %}
{% if response_control.mode != 'SEALED' %}<a class="dispatch-reply-link" href="{% url 'staff_tickets:detail' ticket.public_id %}" aria-label="مشاهده و پاسخ به {{ ticket.customer.customer_profile|default:ticket.customer.username }}" title="مشاهده و پاسخ">{% include 'tickets/_dispatch_icon.html' with icon='reply' %}</a>{% endif %}
</td>
<td data-label="آخرین پاسخ">{% if ticket.latest_customer_response %}<div class="recipient-reply"><p>{{ ticket.latest_customer_response.body|truncatechars:140 }}</p><small>{{ ticket.latest_customer_response.created_at|jalali_datetime }}</small></div>{% else %}<span class="no-reply-state">هنوز پاسخی ثبت نشده</span>{% endif %}</td>
</tr>
{% empty %}<tr><td colspan="6">برای این ارسال تیکتی ثبت نشده است.</td></tr>{% endfor %}
{% empty %}<tr><td colspan="4"><div class="dispatch-empty"><p>برای این ارسال تیکتی ثبت نشده است.</p></div></td></tr>{% endfor %}
</tbody></table></div>
<div class="dispatch-empty" data-recipient-empty hidden><h3>مخاطبی با این شرایط پیدا نشد</h3><p>عبارت جستجو یا فیلتر مخاطبان را تغییر دهید.</p><button type="button" class="dispatch-secondary" data-recipient-reset>نمایش همه مخاطبان</button></div>
</section>
{% if metrics.failed or can_resolve_dispatch %}<section class="dispatch-management" aria-label="اقدام‌های گروهی"><div><h2>اقدام‌های گروهی</h2><p>این اقدام‌ها روی چند مخاطب اثر می‌گذارند؛ برای پاسخ به یک نفر، تیکت او را باز کنید.</p></div><div class="dispatch-management-actions">
{% if metrics.failed %}<form method="post" action="{% url 'staff_tickets:dispatch_retry' dispatch.public_id %}">{% csrf_token %}<button class="dispatch-secondary" type="submit">تلاش مجدد {{ metrics.failed }} پیامک ناموفق</button></form>{% endif %}
{% if can_resolve_dispatch %}{% if open_ticket_count %}<form method="post" action="{% url 'staff_tickets:dispatch_resolve' dispatch.public_id %}" onsubmit="return confirm('با تأیید این عملیات، هر {{ open_ticket_count }} تیکت باز این ارسال گروهی یک‌جا حل‌شده می‌شوند. آیا ادامه می‌دهید؟')">{% csrf_token %}<button class="dispatch-secondary" type="submit">حل‌شدن همه تیکت‌ها ({{ open_ticket_count }})</button><small>پاسخ تازه مشتری می‌تواند تیکت مربوط به خودش را دوباره باز کند.</small></form>{% else %}<p class="dispatch-complete">{% include 'tickets/_dispatch_icon.html' with icon='check' %} رسیدگی این گروه پایان یافته است؛ همه تیکت‌ها حل یا بسته هستند.</p>{% endif %}{% endif %}
</div></section>{% endif %}
</div>
{% endblock %}
{% block extra_scripts %}<script defer src="{% static 'js/dispatch-workspace.js' %}?v={{ static_asset_version }}"></script>{% endblock %}