share ticket and ticket message

This commit is contained in:
javad committed 2026-10-04 16:42:19 +03:30
1 parent 39e64854b8
commit 79fe1cddbb
35 files changed
+1742 -95

No files matched your search

+3 -2
View File
@@ -29,7 +29,7 @@
<a class="{% if request.resolver_match.url_name == 'list' and active_tab == 'new' %}active{% endif %}" href="{% url 'staff_tickets:list' %}?tab=new" {% if request.resolver_match.url_name == 'list' and active_tab == 'new' %}aria-current="page"{% endif %}><span>◌</span><b>صف جدید</b></a>
<a class="staff-action-link {% if request.resolver_match.url_name == 'list' and active_tab == 'waiting_staff' %}active{% endif %}" href="{% url 'staff_tickets:list' %}?tab=waiting_staff" {% if request.resolver_match.url_name == 'list' and active_tab == 'waiting_staff' %}aria-current="page"{% endif %}><span>↺</span><b>در انتظار پاسخ من</b>{% if counts.waiting %}<em>{{ counts.waiting }}</em>{% endif %}</a>
<a class="{% if request.resolver_match.url_name == 'list' and active_tab == 'mine' %}active{% endif %}" href="{% url 'staff_tickets:list' %}?tab=mine" {% if request.resolver_match.url_name == 'list' and active_tab == 'mine' %}aria-current="page"{% endif %}><span>◎</span><b>تیکت‌های من</b></a>
<a class="{% if request.resolver_match.url_name == 'list' and active_tab == 'shared' %}active{% endif %}" href="{% url 'staff_tickets:list' %}?tab=shared" {% if request.resolver_match.url_name == 'list' and active_tab == 'shared' %}aria-current="page"{% endif %}><span>◇</span><b>اشتراک‌ها</b></a>
<a class="{% if request.resolver_match.url_name == 'list' and active_tab == 'shared' %}active{% endif %}" href="{% url 'staff_tickets:list' %}?tab=shared" {% if request.resolver_match.url_name == 'list' and active_tab == 'shared' %}aria-current="page"{% endif %}><span>◇</span><b>اشتراک‌ها</b><em data-share-unread-count data-count-url="{% url 'staff_tickets:shares_count' %}"{% if not staff_unread_shares %} hidden{% endif %} aria-label="اشتراک خوانده‌نشده">{{ staff_unread_shares }}</em></a>
{% if staff_operations_access.contacts %}<a class="{% if active_staff_section == 'contacts' %}active{% endif %}" href="{% url 'staff_tickets:contacts' %}" {% if active_staff_section == 'contacts' %}aria-current="page"{% endif %}><span>♧</span><b>مخاطبان</b></a>{% endif %}
{% if staff_operations_access.dispatches %}<a class="{% if active_staff_section == 'dispatches' %}active{% endif %}" href="{% url 'staff_tickets:dispatch_create' %}" {% if active_staff_section == 'dispatches' %}aria-current="page"{% endif %}><span>⇶</span><b>ارسال گروهی</b></a>{% endif %}
{% if staff_operations_access.audit %}<a class="{% if active_staff_section == 'audit' %}active{% endif %}" href="{% url 'staff_tickets:magic_audit' %}" {% if active_staff_section == 'audit' %}aria-current="page"{% endif %}><span>⌁</span><b>ممیزی لینک‌ها</b></a>{% endif %}
@@ -53,7 +53,7 @@
<a class="{% if request.resolver_match.url_name == 'list' and active_tab == 'new' %}active{% endif %}" href="{% url 'staff_tickets:list' %}?tab=new" {% if request.resolver_match.url_name == 'list' and active_tab == 'new' %}aria-current="page"{% endif %}><span class="dock-icon">◌</span>صف جدید</a>
<a class="{% if request.resolver_match.url_name == 'list' and active_tab == 'waiting_staff' %}active{% endif %}" href="{% url 'staff_tickets:list' %}?tab=waiting_staff" {% if request.resolver_match.url_name == 'list' and active_tab == 'waiting_staff' %}aria-current="page"{% endif %}><span class="dock-icon">↺</span>پاسخ من</a>
<a class="{% if request.resolver_match.url_name == 'list' and active_tab == 'mine' %}active{% endif %}" href="{% url 'staff_tickets:list' %}?tab=mine" {% if request.resolver_match.url_name == 'list' and active_tab == 'mine' %}aria-current="page"{% endif %}><span class="dock-icon">◎</span>تیکت‌های من</a>
<a class="{% if request.resolver_match.url_name == 'list' and active_tab == 'shared' %}active{% endif %}" href="{% url 'staff_tickets:list' %}?tab=shared" {% if request.resolver_match.url_name == 'list' and active_tab == 'shared' %}aria-current="page"{% endif %}><span class="dock-icon">◇</span>اشتراک‌ها</a>
<a class="{% if request.resolver_match.url_name == 'list' and active_tab == 'shared' %}active{% endif %}" href="{% url 'staff_tickets:list' %}?tab=shared" {% if request.resolver_match.url_name == 'list' and active_tab == 'shared' %}aria-current="page"{% endif %}><span class="dock-icon">◇</span>اشتراک‌ها<em data-share-unread-count data-count-url="{% url 'staff_tickets:shares_count' %}"{% if not staff_unread_shares %} hidden{% endif %}>{{ staff_unread_shares }}</em></a>
{% if staff_operations_access.contacts %}<a class="{% if active_staff_section == 'contacts' %}active{% endif %}" href="{% url 'staff_tickets:contacts' %}" {% if active_staff_section == 'contacts' %}aria-current="page"{% endif %}><span class="dock-icon">♧</span>مخاطبان</a>{% endif %}
{% if staff_operations_access.dispatches %}<a class="{% if active_staff_section == 'dispatches' %}active{% endif %}" href="{% url 'staff_tickets:dispatch_create' %}" {% if active_staff_section == 'dispatches' %}aria-current="page"{% endif %}><span class="dock-icon">⇶</span>گروهی</a>{% endif %}
</nav>
@@ -64,6 +64,7 @@
{% block pwa_install_prompt %}{% endblock %}
<script src="{% static 'js/voice-recorder.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/ui.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/chat-coach.js' %}?v={{ static_asset_version }}"></script>
{% if user.is_authenticated %}<script defer src="{% static 'js/pwa.js' %}?v={{ static_asset_version }}"></script>{% endif %}
{% if user.is_authenticated and user.account_type == 'STAFF' %}<link rel="stylesheet" href="{% static 'css/staff-sharing.css' %}?v={{ static_asset_version }}"><script defer src="{% static 'js/staff-sharing.js' %}?v={{ static_asset_version }}"></script>{% endif %}
{% block extra_scripts %}{% endblock %}
{% block otp_script %}{% endblock %}
</body></html>
@@ -0,0 +1,18 @@
{% if can_share_message and not item.deleted_at %}
<details class="staff-message-share" data-share-message="{{ item.public_id }}">
<summary aria-label="اشتراک فقط این پیام؛ گیرندگان و سوابق" title="اشتراک و پیگیری این پیام">{% include 'tickets/_staff_share_icon.html' %}{% if item.staff_message_shares %}<span class="staff-share-dot" aria-hidden="true"></span>{% endif %}</summary>
<div class="staff-share-content">
<header class="staff-inline-share-heading"><b>اشتراک پیام</b><small>فقط همین پیام و پیوست‌ها</small></header>
{% if item.staff_message_shares %}
<div class="staff-inline-share-grants">{% for grant in item.staff_message_shares %}{% include 'tickets/_staff_share_audit_row.html' with is_message=True inline=True %}{% endfor %}</div>
<details class="staff-inline-share-new"><summary>+ اشتراک جدید<span class="staff-share-chevron" aria-hidden="true"></span></summary>{% include 'tickets/_staff_message_share_form.html' %}</details>
{% else %}{% include 'tickets/_staff_message_share_form.html' %}{% endif %}
{% if item.staff_share_events %}
{% load jalali %}
<details class="staff-inline-share-history"><summary>سوابق<span class="staff-share-count">{{ item.staff_share_events|length }}</span><span class="staff-share-chevron" aria-hidden="true"></span></summary>
{% for event in item.staff_share_events %}<article class="share-event-row"><b>{{ event.get_event_type_display }}</b><small>{{ event.actor_name }} · {{ event.created_at|jalali_datetime }}</small>{% if event.recipient_name %}<small>گیرنده: {{ event.recipient_name }}</small>{% endif %}{% if event.metadata.action == 'updated' %}<small>توضیح به‌روزرسانی شد</small>{% endif %}{% if event.metadata.note %}<p>{{ event.metadata.note }}</p>{% endif %}</article>{% endfor %}
</details>
{% endif %}
</div>
</details>
{% endif %}
@@ -0,0 +1,7 @@
<form class="staff-share-form" method="post" action="{% url 'staff_tickets:share_message' ticket.public_id item.public_id %}">
{% csrf_token %}
<label for="share-expert-{{ item.pk }}">کارشناس گیرنده</label>
<select id="share-expert-{{ item.pk }}" name="expert" required><option value="">انتخاب کارشناس</option>{% for expert in share_experts %}<option value="{{ expert.pk }}">{{ expert }}{% if expert.primary_unit %} — {{ expert.primary_unit }}{% endif %}</option>{% endfor %}</select>
<details class="staff-share-note-field"><summary>افزودن توضیح<span class="staff-share-chevron" aria-hidden="true"></span></summary><label for="share-note-{{ item.pk }}">توضیح برای گیرنده</label><textarea id="share-note-{{ item.pk }}" name="note" maxlength="500" rows="2" placeholder="مثلاً: لطفاً این رسید را بررسی کنید"></textarea></details>
<div class="staff-share-footer"><button class="staff-share-submit" type="submit" aria-label="اشتراک پیام و پیوست‌هایش">اشتراک</button></div>
</form>
+26
View File
@@ -0,0 +1,26 @@
{% load jalali %}{% for item in conversation %}
<article class="message {% if item.is_internal %}note{% elif item.author == viewer_user %}mine{% endif %}" data-message data-message-id="{{ item.public_id }}" data-date="{{ item.created_at|date:'Y-m-d' }}" data-day-label="{{ item.created_at|jalali_date }}"{% if item.author == viewer_user and not item.is_internal and not item.deleted_at %} data-chat-coach-own data-chat-coach-at="{{ item.created_at|date:'U' }}"{% endif %}>
<div class="message-avatar" aria-hidden="true">{{ item.author_name|slice:':1' }}</div>
<div class="message-bubble">
<header><b>{{ item.author_name }}</b>{% if item.is_internal %}<span class="internal-label">🔒 یادداشت داخلی</span>{% endif %}<time title="{{ item.created_at|jalali_datetime }}">{{ item.created_at|persian_time }}</time></header>
{% if item.deleted_at and not perms.messaging.view_deleted_message_content %}
<p class="deleted-message">این پیام توسط فرستنده حذف شده است.</p>
{% else %}
{% if item.body %}<p>{{ item.body|linebreaksbr }}</p>{% endif %}
{% if item.deleted_at %}<small class="danger">حذف‌شده در {{ item.deleted_at|jalali_datetime }}</small>{% endif %}
{% if item.edited_at %}<span class="edited-label">ویرایش شده</span>{% if perms.messaging.view_message_revisions and not limited_share %}<a class="revision-link" href="{% url 'messages:revisions' item.public_id %}">تاریخچه</a>{% endif %}{% endif %}
{% if item.attachments.all %}<div class="attachment-list">{% for file in item.attachments.all %}
{% if file.attachment_type == 'IMAGE' %}<div class="attachment attachment-image"><button type="button" data-image-open data-image-src="{% if limited_share %}{% url 'staff_tickets:shared_message_attachment' item.received_share.public_id file.public_id %}{% else %}{% url 'attachments:view' file.public_id %}{% endif %}" data-image-name="{{ file.original_name }}"><img src="{% if limited_share %}{% url 'staff_tickets:shared_message_attachment' item.received_share.public_id file.public_id %}{% else %}{% url 'attachments:view' file.public_id %}{% endif %}" alt="{{ file.original_name }}"><span>{{ file.original_name }} · {{ file.file_size|filesizeformat }}</span></button><a class="attachment-download" href="{% if limited_share %}{% url 'staff_tickets:shared_message_attachment_download' item.received_share.public_id file.public_id %}{% else %}{% url 'attachments:download' file.public_id %}{% endif %}" download>⇩ دانلود تصویر</a></div>
{% elif file.attachment_type == 'VOICE' %}<div class="attachment attachment-audio"><span>🎙 پیام صوتی</span><audio controls preload="metadata" src="{% if limited_share %}{% url 'staff_tickets:shared_message_attachment' item.received_share.public_id file.public_id %}{% else %}{% url 'attachments:view' file.public_id %}{% endif %}"></audio><a href="{% if limited_share %}{% url 'staff_tickets:shared_message_attachment_download' item.received_share.public_id file.public_id %}{% else %}{% url 'attachments:download' file.public_id %}{% endif %}">دریافت فایل</a></div>
{% else %}<a class="attachment attachment-file" href="{% if limited_share %}{% url 'staff_tickets:shared_message_attachment_download' item.received_share.public_id file.public_id %}{% else %}{% url 'attachments:download' file.public_id %}{% endif %}"><span class="file-icon">⇩</span><span><b>{{ file.original_name }}</b><small>{{ file.get_attachment_type_display }} · {{ file.file_size|filesizeformat }}</small></span></a>{% endif %}
{% endfor %}</div>{% endif %}
{% endif %}
{% if item.received_share %}<small class="received-share-attribution">{% include "tickets/_staff_share_icon.html" %} اشتراک توسط {{ item.received_share.granted_by.staff_profile.display_name|default:item.received_share.granted_by.username }} · <time>{{ item.received_share.created_at|jalali_datetime }}</time>{% if item.received_share.note %}<details><summary aria-label="توضیح اشتراک">ⓘ</summary>{{ item.received_share.note }}</details>{% endif %}</small>{% endif %}
<div class="staff-message-footer">
{% if not limited_share %}{% with receipts=item.read_receipts.all %}{% if receipts %}<details class="message-receipts"><summary>✓ دیده‌شده توسط {{ receipts|length }} نفر</summary><ul>{% for receipt in receipts %}<li><span>{{ receipt.reader_name }}</span><time>{{ receipt.read_at|jalali_datetime }}</time></li>{% endfor %}</ul></details>{% endif %}{% endwith %}{% endif %}
{% if item.author == user and not item.deleted_at and not limited_share %}<details class="message-menu" data-message-menu><summary aria-label="عملیات پیام" title="عملیات پیام"><span aria-hidden="true">•••</span></summary><div class="message-menu-panel" role="dialog" aria-label="عملیات پیام"><div class="message-menu-choices" data-message-menu-choices><button type="button" class="message-choice message-choice-edit" data-message-edit-open><span class="message-choice-icon" aria-hidden="true">✎</span><span><b>ویرایش</b><small>اصلاح متن پیام</small></span><span class="message-choice-arrow" aria-hidden="true">‹</span></button><form method="post" action="{% url 'messages:delete' item.public_id %}" onsubmit="return confirm('از حذف این پیام مطمئن هستید؟')">{% csrf_token %}<input type="hidden" name="next" value="{{ request.path }}"><button type="submit" class="message-choice message-choice-delete"><span class="message-choice-icon" aria-hidden="true">⌫</span><span><b>حذف</b><small>حذف پیام از گفتگو</small></span></button></form></div><div class="message-editor" data-message-editor hidden><header class="message-menu-head"><button type="button" class="message-editor-back" data-message-edit-back aria-label="بازگشت به عملیات">→</button><div><b>ویرایش پیام</b><small>متن اصلاح‌شده در تاریخچه ثبت می‌شود</small></div><button type="button" class="message-menu-close" data-message-menu-close aria-label="بستن">×</button></header><form class="message-edit-form" method="post" action="{% url 'messages:edit' item.public_id %}">{% csrf_token %}<label for="edit-{{ item.public_id }}">متن پیام</label><textarea id="edit-{{ item.public_id }}" name="body" maxlength="4000" data-message-edit-input data-original-value="{{ item.body }}">{{ item.body }}</textarea><div class="message-edit-meta"><span>متن را دقیق و کوتاه نگه دارید</span><span data-message-edit-count>{{ item.body|length }} / ۴۰۰۰</span></div><input type="hidden" name="next" value="{{ request.path }}"><div class="message-menu-actions"><button type="submit" class="message-edit-save">ذخیره تغییرات</button><button type="button" class="message-edit-cancel" data-message-menu-close>انصراف</button></div></form></div></div></details>{% endif %}
{% include 'tickets/_staff_message_share.html' %}
</div>
</div>
</article>
{% empty %}<div class="conversation-empty"><span>✦</span><h3>هنوز مکاتبه‌ای ثبت نشده است</h3><p>شرح کامل درخواست یا پاسخ خود را در فرم پایین ثبت کنید.</p></div>{% endfor %}
@@ -0,0 +1,2 @@
{% if has_older %}<button class="load-older" type="button" data-load-older hx-get="{% url 'staff_tickets:messages' ticket.public_id %}?before={{ older_cursor }}" hx-target="this" hx-swap="outerHTML">↑ نمایش مکاتبات قدیمی‌تر</button>{% endif %}
{% include 'tickets/_staff_messages.html' %}
@@ -0,0 +1,17 @@
{% load jalali %}
<details class="share-audit-row">
<summary>
<span class="staff-share-person"><b>{{ grant.user.staff_profile|default:grant.user.username }}</b>{% if not inline %}<small>{% if is_message %}فقط یک پیام{% else %}{{ grant.get_permission_level_display }}{% endif %}</small>{% endif %}</span>
<span class="staff-share-status {% if grant.revoked_at %}is-revoked{% elif is_message and grant.message.deleted_at %}is-revoked{% elif grant.first_viewed_at %}is-seen{% else %}is-pending{% endif %}">{% if grant.revoked_at %}لغوشده{% elif is_message and grant.message.deleted_at %}پیام حذف‌شده{% elif grant.first_viewed_at %}دیده‌شده{% else %}هنوز دیده نشده{% endif %}</span>
<span class="staff-share-chevron" aria-hidden="true"></span>
</summary>
<div class="staff-share-row-content">
{% if is_message and not inline and not grant.message.deleted_at %}<p class="staff-share-preview">{{ grant.message.body|truncatechars:100|default:'پیام دارای پیوست' }}</p>{% endif %}
<small>اشتراک توسط {{ grant.granted_by.staff_profile|default:grant.granted_by.username }} · {{ grant.created_at|jalali_datetime }}</small>
{% if grant.note %}<p class="staff-share-note">{{ grant.note }}</p>{% endif %}
{% if grant.first_viewed_at %}<small>اولین مشاهده: {{ grant.first_viewed_at|jalali_datetime }}</small><small>آخرین مشاهده: {{ grant.last_viewed_at|jalali_datetime }} · {{ grant.view_count }} بار</small>{% endif %}
{% if is_message and not inline %}<small>زمان پیام: {{ grant.message.created_at|jalali_datetime }}</small><small class="staff-share-reference" dir="ltr">{{ grant.message.public_id }}</small>{% endif %}
{% if grant.revoked_at %}<small>لغو: {{ grant.revoked_at|jalali_datetime }}{% if grant.revoked_by %} توسط {{ grant.revoked_by.staff_profile|default:grant.revoked_by.username }}{% endif %}</small>
{% else %}<form method="post" action="{% if is_message %}{% url 'staff_tickets:revoke_message_share' ticket.public_id grant.public_id %}{% else %}{% url 'staff_tickets:revoke' ticket.public_id grant.user.public_id %}{% endif %}">{% csrf_token %}<button class="staff-share-revoke" type="submit">{% if is_message %}لغو اشتراک پیام{% else %}لغو اشتراک تیکت{% endif %}</button></form>{% endif %}
</div>
</details>
+1
View File
@@ -0,0 +1 @@
<svg class="staff-share-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 10.5 6.8-4M8.6 13.5l6.8 4"/></svg>

After

Width:  |  Height:  |  Size: 309 B

@@ -0,0 +1,36 @@
{% load jalali %}
{% if can_share %}
<details class="info-card staff-sharing-audit">
<summary class="staff-sharing-audit-toggle">
<svg class="staff-share-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
<span><strong>پیگیری اشتراک‌ها</strong><small>وضعیت مشاهده و سوابق</small></span>
<span class="staff-share-chevron" aria-hidden="true"></span>
</summary>
<div class="staff-sharing-audit-content">
<p class="staff-share-hint">«دیده‌شده» یعنی بازکردن صفحه، نه تأیید بررسی. لغو اشتراک، دسترسی مستقل سازمانی را حذف نمی‌کند.</p>
<details class="staff-share-group" data-share-scope="ticket">
<summary><span>اشتراک کل تیکت</span><span class="staff-share-count">{{ grants|length }}</span><span class="staff-share-chevron" aria-hidden="true"></span></summary>
{% for grant in grants %}{% include 'tickets/_staff_share_audit_row.html' with is_message=False %}{% empty %}<p class="staff-share-empty">اشتراکی ثبت نشده است.</p>{% endfor %}
</details>
<details class="staff-share-group" data-share-scope="message">
<summary><span>اشتراک پیام‌ها</span><span class="staff-share-count">{{ message_shares|length }}</span><span class="staff-share-chevron" aria-hidden="true"></span></summary>
{% for grant in message_shares %}{% include 'tickets/_staff_share_audit_row.html' with is_message=True %}{% empty %}<p class="staff-share-empty">پیامی به اشتراک گذاشته نشده است.</p>{% endfor %}
</details>
<details class="staff-share-group staff-share-events">
<summary><span>سوابق اشتراک و انتقال</span><span class="staff-share-count">{{ sharing_events|length }}</span><span class="staff-share-chevron" aria-hidden="true"></span></summary>
<p class="staff-share-hint">۱۰۰ رویداد اخیر</p>
{% for event in sharing_events %}
<article class="share-event-row">
<b>{{ event.get_event_type_display }}</b><small>{{ event.actor_name }} · {{ event.created_at|jalali_datetime }}</small>
{% if event.recipient_name %}<span>گیرنده: {{ event.recipient_name }}</span>{% endif %}
{% if event.previous_owner_name %}<small>کارشناس قبلی: {{ event.previous_owner_name }}</small>{% endif %}
{% if event.metadata.action == 'updated' %}<span>توضیح یا سطح دسترسی به‌روزرسانی شد</span>{% endif %}
{% if event.metadata.level %}<small>سطح دسترسی ثبت‌شده: {{ event.metadata.level }}</small>{% endif %}
{% if event.metadata.message_public_id %}<small class="staff-share-reference" dir="ltr">{{ event.metadata.message_public_id }}</small>{% endif %}
{% if event.metadata.note %}<p>{{ event.metadata.note }}</p>{% endif %}
</article>
{% empty %}<p class="staff-share-empty">رویدادی ثبت نشده است.</p>{% endfor %}
</details>
</div>
</details>
{% endif %}
+1 -1
View File
@@ -4,7 +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.owner and ticket.owner != user and not perms.tickets.view_all_tickets %}
{% 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 %}
<div class="ticket-row-meta"><span>{{ ticket.category }}</span><span>{{ ticket.customer.customer_profile|default:ticket.customer.mobile }}</span><span>{% if ticket.city %}{{ ticket.city }}، {{ ticket.province }}{% elif ticket.province %}{{ ticket.province }}{% else %}موقعیت ثبت نشده{% endif %}</span></div>
@@ -0,0 +1,15 @@
{% if can_share %}
<details class="staff-ticket-share">
<summary>{% include 'tickets/_staff_share_icon.html' %}<span>اشتراک با کارشناس</span><span class="staff-share-chevron" aria-hidden="true"></span></summary>
<div class="staff-share-content">
<p class="staff-share-hint">دسترسی به کل تیکت. برای ارسال یک رسید یا تصویر، «اشتراک فقط این پیام» را انتخاب کنید.</p>
<form class="staff-share-form" method="post" action="{% url 'staff_tickets:share' ticket.public_id %}">
{% csrf_token %}
<label for="{{ share_form.expert.id_for_label }}">کارشناس گیرنده</label>{{ share_form.expert }}
<label for="{{ share_form.permission_level.id_for_label }}">سطح دسترسی</label>{{ share_form.permission_level }}
<label for="{{ share_form.note.id_for_label }}">توضیح برای گیرنده</label>{{ share_form.note }}
<div class="staff-share-footer"><small>توضیح اختیاری است</small><button class="staff-share-submit" type="submit">اشتراک کل تیکت</button></div>
</form>
</div>
</details>
{% endif %}
+7 -4
View File
@@ -1,7 +1,7 @@
{% extends 'base.html' %}{% load static jalali %}
{% block title %}{{ page_title }}{% endblock %}
{% block content %}
<div class="ticket-workspace staff-ticket-workspace" data-conversation-workspace data-poll-url="{% url 'staff_tickets:poll' ticket.public_id %}" data-poll-version="{{ poll_version }}" data-poll-cursor="{{ poll_cursor }}" data-draft-key="ticket-{{ ticket.public_id }}-staff" data-reply-result="{{ request.GET.reply_result|default:'' }}">
<div class="ticket-workspace staff-ticket-workspace" data-conversation-workspace data-share-read-url="{% url 'staff_tickets:read_shares' ticket.public_id %}" data-poll-url="{% url 'staff_tickets:poll' ticket.public_id %}" data-poll-version="{{ poll_version }}" data-poll-cursor="{{ poll_cursor }}" data-draft-key="ticket-{{ ticket.public_id }}-staff" data-reply-result="{{ request.GET.reply_result|default:'' }}">
<header class="ticket-workspace-head">
<div class="ticket-title"><a class="workspace-back" href="{% url 'staff_tickets:list' %}" 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>{% include 'tickets/_voip_call_button.html' with customer=ticket.customer %}{% if can_claim %}<form method="post" action="{% url 'staff_tickets:claim' ticket.public_id %}">{% csrf_token %}<button type="submit">اختصاص به من</button></form>{% endif %}<button type="button" class="icon-button" data-toggle-panel="ticket-actions" aria-label="عملیات تیکت">⚙</button><button type="button" class="icon-button" data-toggle-panel="customer-info" aria-label="اطلاعات مشتری">◉</button></div>
@@ -20,14 +20,15 @@
{% if sms_jobs %}<section class="info-card sms-delivery-card"><div class="sms-delivery-heading"><div><h3>وضعیت پیامک‌های مشتری</h3><small>خلاصه ارسال و تحویل</small></div><span aria-hidden="true">ⓘ</span></div><div class="sms-delivery-list">{% for sms_job in sms_jobs %}<details class="sms-delivery-item"><summary><span class="sms-delivery-event">{% if sms_job.event_type == 'STAFF_TICKET_CREATED' %}ثبت تیکت توسط کارشناس{% elif sms_job.event_type == 'CUSTOMER_TICKET_CREATED' %}ثبت تیکت مشتری{% elif sms_job.event_type == 'STAFF_REPLY' %}اعلان پاسخ کارشناس{% else %}پیامک سیستمی{% endif %}</span><b>{% if sms_job.delivery_status %}{{ sms_job.get_delivery_status_display }}{% else %}{{ sms_job.get_status_display }}{% endif %}</b><span class="sms-delivery-info" aria-label="نمایش جزئیات زمان‌بندی پیامک" title="نمایش جزئیات">i</span></summary><ol class="sms-delivery-timeline"><li class="is-complete"><span></span><div><b>ثبت درخواست ارسال</b><time datetime="{{ sms_job.created_at|date:'c' }}">{{ sms_job.created_at|jalali_datetime }}</time></div></li><li class="{% if sms_job.sent_at %}is-complete{% endif %}"><span></span><div><b>پذیرش توسط درگاه پیامک</b>{% if sms_job.sent_at %}<time datetime="{{ sms_job.sent_at|date:'c' }}">{{ sms_job.sent_at|jalali_datetime }}</time>{% else %}<small>{% if sms_job.status == 'FAILED' %}ناموفق{% elif sms_job.status == 'CANCELLED' %}لغوشده پیش از ارسال{% else %}در انتظار ارسال{% endif %}</small>{% endif %}</div></li><li class="{% if sms_job.delivery_status == 'DELIVERED' %}is-complete{% endif %}"><span></span><div><b>تأیید رسیدن به گوشی</b>{% if sms_job.delivery_status == 'DELIVERED' and sms_job.delivery_checked_at %}<time datetime="{{ sms_job.delivery_checked_at|date:'c' }}">{{ sms_job.delivery_checked_at|jalali_datetime }}</time>{% elif sms_job.delivery_checked_at %}<small>آخرین استعلام: {{ sms_job.delivery_checked_at|jalali_datetime }}</small>{% else %}<small>{% if sms_job.sent_at %}در انتظار نتیجه اپراتور{% else %}هنوز ارسال نشده{% endif %}</small>{% endif %}</div></li></ol>{% if sms_job.delivery_description %}<p class="sms-delivery-description">{{ sms_job.delivery_description }}</p>{% endif %}{% if sms_job.last_error %}<p class="sms-delivery-error">علت دقیق: {{ sms_job.last_error }}</p>{% endif %}{% if sms_job.provider_message_id %}<small class="sms-delivery-tracking" dir="ltr">شناسه رهگیری: {{ sms_job.provider_message_id }}</small>{% endif %}{% if sms_job.sms_retryable %}<form method="post" action="{% url 'staff_tickets:sms_retry' ticket.public_id sms_job.pk %}" class="inline-retry-form">{% csrf_token %}<button type="submit" aria-label="تلاش مجدد برای ارسال این پیامک" title="تلاش مجدد"><span aria-hidden="true">↻</span> تلاش مجدد</button></form>{% endif %}</details>{% endfor %}</div></section>{% endif %}
{% include 'tickets/_immediate_announcement.html' %}
{% include 'tickets/_announcement_status.html' %}
<section class="info-card ticket-operations"><h3>عملیات تیکت</h3>{% if perms.tickets.change_ticket %}<form method="post" action="{% url 'staff_tickets:status' ticket.public_id %}">{% csrf_token %}<label>وضعیت<select name="status">{% for value,label in statuses %}<option value="{{ value }}" {% if ticket.status == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><button>ثبت وضعیت</button></form>{% endif %}{% if perms.tickets.change_ticket_priority %}<form method="post" action="{% url 'staff_tickets:priority' ticket.public_id %}">{% csrf_token %}<label>اولویت<select name="priority">{% for value,label in priorities %}<option value="{{ value }}" {% if ticket.priority == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><button class="secondary">ثبت اولویت</button></form>{% endif %}{% if can_transfer %}<details><summary>انتقال تیکت</summary><form method="post" action="{% url 'staff_tickets:transfer' ticket.public_id %}">{% csrf_token %}{{ transfer_form.as_p }}<button>انتقال</button></form></details>{% endif %}{% if can_share %}<details><summary>اشتراک با کارشناس</summary><form method="post" action="{% url 'staff_tickets:share' ticket.public_id %}">{% csrf_token %}{{ share_form.as_p }}<button>ایجاد دسترسی</button></form>{% for grant in grants %}<div class="grant"><span>{{ grant.user.staff_profile.display_name }}</span><form method="post" action="{% url 'staff_tickets:revoke' ticket.public_id grant.user.public_id %}">{% csrf_token %}<button class="danger-button">لغو</button></form></div>{% endfor %}</details>{% endif %}</section>
<section class="info-card ticket-operations"><h3>عملیات تیکت</h3>{% if can_share %}<form method="post" action="{% url 'staff_tickets:status' ticket.public_id %}">{% csrf_token %}<label>وضعیت<select name="status">{% for value,label in statuses %}<option value="{{ value }}" {% if ticket.status == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><button>ثبت وضعیت</button></form>{% endif %}{% if can_share %}<form method="post" action="{% url 'staff_tickets:priority' ticket.public_id %}">{% csrf_token %}<label>اولویت<select name="priority">{% for value,label in priorities %}<option value="{{ value }}" {% if ticket.priority == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><button class="secondary">ثبت اولویت</button></form>{% endif %}{% if can_transfer %}<details><summary>انتقال تیکت</summary><form method="post" action="{% url 'staff_tickets:transfer' ticket.public_id %}">{% csrf_token %}{{ transfer_form.as_p }}<button>انتقال</button></form></details>{% endif %}{% include "tickets/_staff_ticket_share.html" %}</section>
{% include 'tickets/_staff_sharing_audit.html' %}
{% if can_delete %}<section class="info-card danger-zone"><h3>حذف کامل تیکت</h3><p class="muted">تمام پیام‌ها، فایل‌ها و سوابق تیکت برای همیشه حذف می‌شوند.</p><form method="post" action="{% url 'staff_tickets:delete' ticket.public_id %}" onsubmit="return confirm('آیا از حذف کامل و غیرقابل‌بازگشت این تیکت مطمئن هستید؟')">{% csrf_token %}<button type="submit" class="danger-button">حذف کامل تیکت</button></form></section>{% endif %}
</aside>
<main class="conversation-pane correspondence-pane">
<section class="conversation-toolbar"><div><b>سوابق رسیدگی و مکاتبات</b></div><small><span data-conversation-count>{{ conversation|length }}</span> مکاتبه</small></section>
<section class="conversation" data-conversation role="log" aria-live="polite">{% if has_older %}<button class="load-older" type="button" data-load-older hx-get="{% url 'staff_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>
<section class="conversation" data-conversation role="log" aria-live="polite">{% if has_older %}<button class="load-older" type="button" data-load-older hx-get="{% url 'staff_tickets:messages' ticket.public_id %}?before={{ older_cursor }}" hx-target="this" hx-swap="outerHTML">↑ نمایش مکاتبات قدیمی‌تر</button>{% endif %}{% include 'tickets/_staff_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 staff-composer {% if request.GET.reply_result != 'error' %}is-collapsed{% endif %}" x-data="{mode:'reply'}" :class="mode==='note'&&'note-mode'" data-composer-shell>
{% if not limited_share %}<section class="composer staff-composer {% if request.GET.reply_result != 'error' %}is-collapsed{% endif %}" x-data="{mode:'reply'}" :class="mode==='note'&&'note-mode'" data-composer-shell>
<button type="button" class="composer-launch" data-composer-toggle aria-expanded="false"><span class="composer-launch-icon">+</span><span><b>ثبت اقدام جدید در تیکت</b><small>پاسخ رسمی یا یادداشت کارشناسی ثبت کنید</small></span><span class="composer-launch-arrow">↑</span></button>
<div class="mode" role="tablist"><button type="button" role="tab" data-composer-mode="reply" @click="mode='reply'" :class="mode==='reply'&&'active'">پاسخ رسمی به مشتری</button>{% if perms.messaging.add_internal_note %}<button type="button" role="tab" data-composer-mode="note" @click="mode='note'" :class="mode==='note'&&'active note-button'">🔒 یادداشت کارشناسی</button>{% endif %}</div>
<form method="post" enctype="multipart/form-data" data-ticket-composer data-chat-coach data-chat-coach-key="ticket-{{ ticket.public_id }}-staff-{{ user.public_id }}" :action="mode==='note'?'{% url 'staff_tickets:note' ticket.public_id %}':'{% url 'staff_tickets:reply' ticket.public_id %}'">{% csrf_token %}
@@ -39,10 +40,12 @@
<div class="composer-bar"><div class="composer-tools"><button class="tool-button" type="button" data-toggle-recorder>●<span>توضیح صوتی</span></button><button class="tool-button" type="button" data-toggle-extra>@<span>اطلاع به کارشناس</span></button><button class="tool-button composer-close" type="button" data-composer-close>بستن</button></div><small class="composer-hint">پیش از ثبت، متن و پیوست‌ها را بازبینی کنید.</small><button class="send-button" type="submit" data-primary-submit><span x-text="mode==='note'?'ثبت یادداشت در پرونده':'ثبت پاسخ در تیکت'"></span> ←</button></div>
</form>
</section>
{% endif %}
</main>
<aside class="workspace-side customer-side" id="customer-info" data-workspace-panel><button type="button" class="panel-close" data-close-panel>×</button><section class="info-card"><p class="panel-index">مشتری</p><h3>{{ ticket.customer.customer_profile|default:'بدون نام' }}</h3><p class="mt-1 text-sm font-bold" dir="ltr">{{ ticket.customer.mobile }}</p><div class="info-list"><div class="info-row"><span>نوع مشتری</span><b>{{ ticket.customer.customer_profile.get_customer_type_display|default:'ثبت نشده' }}</b></div><div class="info-row"><span>موقعیت تیکت</span><b>{% if ticket.city %}{{ ticket.city }}، {{ ticket.province }}{% elif ticket.province %}{{ ticket.province }}{% else %}ثبت نشده{% endif %}</b></div><div class="info-row"><span>تعداد تیکت</span><b>{{ ticket.customer.customer_tickets.count }}</b></div></div></section><section class="info-card"><h3>سابقه اخیر</h3>{% for old in customer_history %}<div class="history-item"><b>{{ old.subject }}</b><span class="ticket-number">{{ old.ticket_number }}</span>{% if perms.tickets.view_all_tickets or old.owner == user or old.selected_expert == user %}<a class="back-link" href="{% url 'staff_tickets:detail' old.public_id %}">مشاهده ←</a>{% else %}<small class="muted">محتوای خصوصی</small>{% endif %}</div>{% empty %}<p class="muted mt-3">تیکت دیگری ثبت نشده است.</p>{% endfor %}</section></aside>
</div>
</div>
{% include 'tickets/_image_viewer.html' %}
<link rel="stylesheet" href="{% static 'css/staff-sharing.css' %}?v={{ static_asset_version }}">
<script src="{% static 'vendor/compressor.min.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/attachment-uploader.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/image-viewer.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/conversation.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/voip-call.js' %}?v={{ static_asset_version }}"></script>
{% endblock %}