fix some bugs
This commit is contained in:
1 parent
b8b3aba855
commit
963fd839dc
22 files changed
+570
-33
No files matched your search
@@ -23,10 +23,15 @@ def _magic_link(job, reason):
|
||||
token,link = TicketMagicLink.issue_with_record(job.ticket, reason=reason, created_by=job.staff_user or job.ticket.owner or job.ticket.created_by)
|
||||
return link, f"{settings.PUBLIC_BASE_URL}/t/{token}/"
|
||||
|
||||
def _sms_context(job,magic_link):
|
||||
profile=getattr(job.customer,"customer_profile",None)
|
||||
def _sms_context(job,magic_link=""):
|
||||
recipient = job.customer or job.ticket.customer
|
||||
profile=getattr(recipient,"customer_profile",None)
|
||||
unit=job.ticket.owning_unit
|
||||
return {"magic_link":magic_link,"first_name":getattr(profile,"first_name","") or "","last_name":getattr(profile,"last_name","") or "","recipient_name":" ".join(filter(None,[getattr(profile,"first_name","") or "",getattr(profile,"last_name","") or ""])),"unit_name":unit.name if unit else "","company_name":getattr(settings,"COMPANY_NAME","لوله گستر گلپایگان"),"ticket_number":job.ticket.ticket_number,"subject":job.ticket.subject,"province":job.ticket.province or getattr(profile,"province","") or "","city":job.ticket.city or getattr(profile,"city","") or "","expert_name":job.ticket.assigned_expert_name}
|
||||
customer_profile=getattr(job.ticket.customer,"customer_profile",None)
|
||||
first_name=getattr(profile,"first_name","") or ""
|
||||
last_name=getattr(profile,"last_name","") or ""
|
||||
recipient_name=" ".join(filter(None,[first_name,last_name])) or "ثبت نشده"
|
||||
return {"magic_link":magic_link,"first_name":first_name,"last_name":last_name,"recipient_name":recipient_name,"unit_name":unit.name if unit else "","company_name":getattr(settings,"COMPANY_NAME","لوله گستر گلپایگان"),"ticket_number":job.ticket.ticket_number,"subject":job.ticket.subject,"province":job.ticket.province or getattr(customer_profile,"province","") or "","city":job.ticket.city or getattr(customer_profile,"city","") or "","expert_name":job.ticket.assigned_expert_name,"customer_mobile":job.ticket.customer.local_mobile or "ثبت نشده","category_name":job.ticket.category.title}
|
||||
|
||||
def _render_sms(event_type,context,unit=None):
|
||||
from string import Formatter
|
||||
@@ -65,10 +70,7 @@ def _send_sms(job):
|
||||
recipient = job.payload.get("recipient_mobile", "")
|
||||
if not recipient:
|
||||
raise ValueError("Unit supervisor mobile is not configured")
|
||||
subject = " ".join(job.ticket.subject.split())[:80] or "بدون موضوع"
|
||||
category = " ".join(job.ticket.category.title.split())[:50]
|
||||
customer_mobile = job.ticket.customer.local_mobile or "ثبت نشده"
|
||||
message = f"تیکت جدید {job.ticket.ticket_number}\nتلفن: {customer_mobile}\nموضوع: {subject}\nدسته: {category}"
|
||||
message = _render_sms(job.event_type, _sms_context(job), job.ticket.owning_unit)
|
||||
return get_sms_provider().send_message(recipient, message), issued
|
||||
if job.event_type == "CUSTOMER_TICKET_CREATED":
|
||||
issued, url = _magic_link(job, "customer_created_ticket")
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
EVENT = "UNIT_SUPERVISOR_NEW_TICKET"
|
||||
TITLE = "اطلاع تیکت جدید به سرپرستان واحد"
|
||||
BODY = "تیکت جدید {ticket_number}\nتلفن: {customer_mobile}\nموضوع: {subject}\nدسته: {category_name}\nارجاع: {expert_name}"
|
||||
TAGS = [
|
||||
"magic_link", "first_name", "last_name", "recipient_name", "unit_name",
|
||||
"company_name", "ticket_number", "subject", "province", "city",
|
||||
"expert_name", "customer_mobile", "category_name",
|
||||
]
|
||||
|
||||
|
||||
def seed_supervisor_template(apps, schema_editor):
|
||||
sms_template = apps.get_model("notifications", "SMSTemplate")
|
||||
sms_template.objects.get_or_create(
|
||||
unit=None,
|
||||
event_type=EVENT,
|
||||
defaults={"title": TITLE, "body": BODY, "allowed_tags": TAGS, "is_active": True},
|
||||
)
|
||||
for template in sms_template.objects.all().iterator():
|
||||
allowed_tags = list(template.allowed_tags or [])
|
||||
updated_tags = list(dict.fromkeys([*allowed_tags, "customer_mobile", "category_name"]))
|
||||
if updated_tags != allowed_tags:
|
||||
template.allowed_tags = updated_tags
|
||||
template.save(update_fields=["allowed_tags"])
|
||||
|
||||
|
||||
def remove_supervisor_template(apps, schema_editor):
|
||||
apps.get_model("notifications", "SMSTemplate").objects.filter(
|
||||
unit__isnull=True, event_type=EVENT, title=TITLE, body=BODY
|
||||
).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("notifications", "0011_notification_delivery_tracking"),
|
||||
("staff", "0011_organizationalunit_supervisor_mobile_4"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="smstemplate",
|
||||
name="event_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("CUSTOMER_TICKET_CREATED", "ثبت تیکت توسط مشتری"),
|
||||
("STAFF_TICKET_CREATED", "ثبت تیکت توسط کارشناس"),
|
||||
("STAFF_REPLY", "پاسخ کارشناس"),
|
||||
(EVENT, TITLE),
|
||||
],
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(seed_supervisor_template, remove_supervisor_template),
|
||||
]
|
||||
@@ -5,7 +5,7 @@ from apps.core.models import PersianTextNormalizationMixin
|
||||
|
||||
|
||||
def sms_template_tags():
|
||||
return ["magic_link", "first_name", "last_name", "recipient_name", "unit_name", "company_name", "ticket_number", "subject", "province", "city", "expert_name"]
|
||||
return ["magic_link", "first_name", "last_name", "recipient_name", "unit_name", "company_name", "ticket_number", "subject", "province", "city", "expert_name", "customer_mobile", "category_name"]
|
||||
|
||||
class SMSTemplate(PersianTextNormalizationMixin, models.Model):
|
||||
persian_text_fields={"title":False,"body":True}
|
||||
@@ -13,6 +13,7 @@ class SMSTemplate(PersianTextNormalizationMixin, models.Model):
|
||||
CUSTOMER_TICKET_CREATED="CUSTOMER_TICKET_CREATED","ثبت تیکت توسط مشتری"
|
||||
STAFF_TICKET_CREATED="STAFF_TICKET_CREATED","ثبت تیکت توسط کارشناس"
|
||||
STAFF_REPLY="STAFF_REPLY","پاسخ کارشناس"
|
||||
UNIT_SUPERVISOR_NEW_TICKET="UNIT_SUPERVISOR_NEW_TICKET","اطلاع تیکت جدید به سرپرستان واحد"
|
||||
unit=models.ForeignKey("staff.OrganizationalUnit",on_delete=models.CASCADE,null=True,blank=True,related_name="sms_templates",help_text="خالی یعنی الگوی عمومی و fallback.")
|
||||
event_type=models.CharField(max_length=50,choices=Event.choices)
|
||||
title=models.CharField(max_length=120)
|
||||
|
||||
+2
-2
@@ -15,13 +15,13 @@ class UnitMembershipInline(admin.TabularInline):
|
||||
class OrganizationalUnitAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "slug", "bale_chat_id", "sort_order", "show_in_customer_form", "accepts_external_tickets", "is_active", "updated_at")
|
||||
list_filter = ("show_in_customer_form", "accepts_external_tickets", "is_active")
|
||||
search_fields = ("name", "slug", "bale_chat_id", "supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3")
|
||||
search_fields = ("name", "slug", "bale_chat_id", "supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4")
|
||||
list_editable = ("sort_order", "show_in_customer_form", "accepts_external_tickets", "is_active")
|
||||
filter_horizontal = ("permissions",)
|
||||
inlines = (UnitMembershipInline,)
|
||||
fieldsets = (
|
||||
("مشخصات واحد", {"fields": ("name", "slug", "description", "search_keywords", "sort_order")}),
|
||||
("اطلاعرسانی بله و سرپرستان", {"fields": ("bale_chat_id", "supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3")}),
|
||||
("اطلاعرسانی بله و سرپرستان", {"fields": ("bale_chat_id", "supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4")}),
|
||||
("نمایش و دسترسی", {"fields": ("permissions", "show_in_customer_form", "accepts_external_tickets", "is_active")}),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("staff", "0010_organizationalunit_notification_destinations")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="organizationalunit",
|
||||
name="supervisor_mobile_4",
|
||||
field=models.CharField(blank=True, max_length=16, verbose_name="شماره همراه سرپرست چهارم"),
|
||||
),
|
||||
]
|
||||
@@ -36,6 +36,7 @@ class OrganizationalUnit(PersianTextNormalizationMixin, TimeStampedModel):
|
||||
supervisor_mobile_1 = models.CharField(max_length=16, blank=True, verbose_name="شماره همراه سرپرست اول")
|
||||
supervisor_mobile_2 = models.CharField(max_length=16, blank=True, verbose_name="شماره همراه سرپرست دوم")
|
||||
supervisor_mobile_3 = models.CharField(max_length=16, blank=True, verbose_name="شماره همراه سرپرست سوم")
|
||||
supervisor_mobile_4 = models.CharField(max_length=16, blank=True, verbose_name="شماره همراه سرپرست چهارم")
|
||||
|
||||
class Meta:
|
||||
ordering = ["sort_order", "name", "pk"]
|
||||
@@ -52,6 +53,7 @@ class OrganizationalUnit(PersianTextNormalizationMixin, TimeStampedModel):
|
||||
self.supervisor_mobile_1,
|
||||
self.supervisor_mobile_2,
|
||||
self.supervisor_mobile_3,
|
||||
self.supervisor_mobile_4,
|
||||
))))
|
||||
|
||||
def clean(self):
|
||||
@@ -60,7 +62,7 @@ class OrganizationalUnit(PersianTextNormalizationMixin, TimeStampedModel):
|
||||
|
||||
normalized = []
|
||||
errors = {}
|
||||
for field_name in ("supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3"):
|
||||
for field_name in ("supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4"):
|
||||
value = getattr(self, field_name, "").strip()
|
||||
if not value:
|
||||
setattr(self, field_name, "")
|
||||
@@ -80,7 +82,7 @@ class OrganizationalUnit(PersianTextNormalizationMixin, TimeStampedModel):
|
||||
def save(self, *args, **kwargs):
|
||||
from apps.accounts.models import normalize_mobile
|
||||
|
||||
for field_name in ("supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3"):
|
||||
for field_name in ("supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4"):
|
||||
value = getattr(self, field_name, "").strip()
|
||||
setattr(self, field_name, normalize_mobile(value) if value else "")
|
||||
self.bale_chat_id = self.bale_chat_id.strip()
|
||||
|
||||
@@ -112,6 +112,46 @@ def refresh_group_dispatch_status(dispatch_id):
|
||||
dispatch.status=status
|
||||
return dispatch
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def resolve_group_dispatch_tickets(*, dispatch, user):
|
||||
"""Resolve every non-final ticket created by one group dispatch."""
|
||||
dispatch = GroupDispatch.objects.select_for_update().select_related("unit").get(pk=dispatch.pk)
|
||||
permitted = user.has_perm("tickets.change_ticket") or TicketPolicy.has_unit_permission(
|
||||
user, dispatch.unit, "tickets.change_ticket"
|
||||
)
|
||||
if not (DispatchPolicy.can_view(user, dispatch) and permitted):
|
||||
raise PermissionDenied("اجازه حلکردن تیکتهای این ارسال گروهی را ندارید.")
|
||||
|
||||
now = timezone.now()
|
||||
tickets = list(
|
||||
dispatch.tickets.select_for_update().exclude(
|
||||
status__in=(Ticket.Status.RESOLVED, Ticket.Status.CLOSED)
|
||||
)
|
||||
)
|
||||
if not tickets:
|
||||
return 0
|
||||
previous_statuses = {ticket.pk: ticket.status for ticket in tickets}
|
||||
for ticket in tickets:
|
||||
ticket.status = Ticket.Status.RESOLVED
|
||||
ticket.resolved_at = now
|
||||
ticket.updated_at = now
|
||||
Ticket.objects.bulk_update(tickets, ["status", "resolved_at", "updated_at"])
|
||||
TicketEvent.objects.bulk_create([
|
||||
TicketEvent(
|
||||
ticket=ticket,
|
||||
event_type=TicketEvent.Type.STATUS_CHANGED,
|
||||
actor=user,
|
||||
metadata={
|
||||
"from": previous_statuses[ticket.pk],
|
||||
"to": Ticket.Status.RESOLVED,
|
||||
"group_dispatch_id": dispatch.pk,
|
||||
},
|
||||
)
|
||||
for ticket in tickets
|
||||
])
|
||||
return len(tickets)
|
||||
|
||||
@transaction.atomic
|
||||
def claim_ticket(ticket_id,user):
|
||||
ticket=Ticket.objects.select_for_update().get(pk=ticket_id)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
app_name="staff_tickets"
|
||||
urlpatterns=[path("",views.staff_list,name="list"),path("customers/",views.staff_customers,name="customers"),path("customers/search/",views.staff_customer_search,name="customer_search"),path("contacts/",views.staff_contacts,name="contacts"),path("contacts/new/",views.staff_contact_create,name="contact_create"),path("contacts/<uuid:public_id>/edit/",views.staff_contact_edit,name="contact_edit"),path("contact-groups/",views.staff_groups,name="groups"),path("contact-groups/<int:group_id>/",views.staff_groups,name="group_edit"),path("magic-links/",views.staff_magic_audit,name="magic_audit"),path("magic-links/<int:link_id>/revoke/",views.staff_magic_revoke,name="magic_revoke"),path("sms-templates/",views.staff_sms_templates,name="sms_templates"),path("sms-templates/<int:template_id>/",views.staff_sms_templates,name="sms_template_edit"),path("dispatches/",views.staff_dispatch_create,name="dispatch_create"),path("dispatches/<uuid:public_id>/",views.staff_dispatch_detail,name="dispatch_detail"),path("dispatches/<uuid:public_id>/retry/",views.staff_dispatch_retry,name="dispatch_retry"),path("experts/<uuid:public_id>/avatar/",views.expert_avatar,name="expert_avatar"),path("tickets/",views.staff_list,name="tickets"),path("tickets/new/",views.staff_create,name="create"),path("tickets/<uuid:public_id>/messages/older/",views.staff_messages,name="messages"),path("tickets/<uuid:public_id>/messages/poll/",views.staff_poll,name="poll"),path("tickets/<uuid:public_id>/",views.staff_detail,name="detail"),path("tickets/<uuid:public_id>/claim/",views.staff_claim,name="claim"),path("tickets/<uuid:public_id>/reply/",views.staff_reply,name="reply"),path("tickets/<uuid:public_id>/note/",lambda r,public_id:views.staff_reply(r,public_id,note=True),name="note"),path("tickets/<uuid:public_id>/transfer/",views.staff_transfer,name="transfer"),path("tickets/<uuid:public_id>/share/",views.staff_share,name="share"),path("tickets/<uuid:public_id>/revoke/<uuid:user_id>/",views.staff_revoke,name="revoke"),path("tickets/<uuid:public_id>/status/",views.staff_status,name="status"),path("tickets/<uuid:public_id>/priority/",views.staff_priority,name="priority"),path("tickets/<uuid:public_id>/delete/",views.staff_delete,name="delete")]
|
||||
urlpatterns=[path("",views.staff_list,name="list"),path("customers/",views.staff_customers,name="customers"),path("customers/search/",views.staff_customer_search,name="customer_search"),path("contacts/",views.staff_contacts,name="contacts"),path("contacts/new/",views.staff_contact_create,name="contact_create"),path("contacts/<uuid:public_id>/edit/",views.staff_contact_edit,name="contact_edit"),path("contact-groups/",views.staff_groups,name="groups"),path("contact-groups/<int:group_id>/",views.staff_groups,name="group_edit"),path("magic-links/",views.staff_magic_audit,name="magic_audit"),path("magic-links/<int:link_id>/revoke/",views.staff_magic_revoke,name="magic_revoke"),path("sms-templates/",views.staff_sms_templates,name="sms_templates"),path("sms-templates/<int:template_id>/",views.staff_sms_templates,name="sms_template_edit"),path("dispatches/",views.staff_dispatch_create,name="dispatch_create"),path("dispatches/<uuid:public_id>/",views.staff_dispatch_detail,name="dispatch_detail"),path("dispatches/<uuid:public_id>/retry/",views.staff_dispatch_retry,name="dispatch_retry"),path("dispatches/<uuid:public_id>/resolve/",views.staff_dispatch_resolve,name="dispatch_resolve"),path("experts/<uuid:public_id>/avatar/",views.expert_avatar,name="expert_avatar"),path("tickets/",views.staff_list,name="tickets"),path("tickets/new/",views.staff_create,name="create"),path("tickets/<uuid:public_id>/messages/older/",views.staff_messages,name="messages"),path("tickets/<uuid:public_id>/messages/poll/",views.staff_poll,name="poll"),path("tickets/<uuid:public_id>/",views.staff_detail,name="detail"),path("tickets/<uuid:public_id>/claim/",views.staff_claim,name="claim"),path("tickets/<uuid:public_id>/reply/",views.staff_reply,name="reply"),path("tickets/<uuid:public_id>/note/",lambda r,public_id:views.staff_reply(r,public_id,note=True),name="note"),path("tickets/<uuid:public_id>/transfer/",views.staff_transfer,name="transfer"),path("tickets/<uuid:public_id>/share/",views.staff_share,name="share"),path("tickets/<uuid:public_id>/revoke/<uuid:user_id>/",views.staff_revoke,name="revoke"),path("tickets/<uuid:public_id>/status/",views.staff_status,name="status"),path("tickets/<uuid:public_id>/priority/",views.staff_priority,name="priority"),path("tickets/<uuid:public_id>/delete/",views.staff_delete,name="delete")]
|
||||
+24
-4
@@ -31,7 +31,7 @@ from .forms import ContactGroupForm,GroupDispatchForm,ReplyForm,ShareForm,StaffC
|
||||
from .models import GroupDispatch,Ticket,TicketCategory,TicketReadState,TicketTag
|
||||
from .policies import DispatchPolicy, TicketPolicy
|
||||
from .selectors import customer_tickets,dashboard_counts,filter_staff_tickets,staff_ticket_rows
|
||||
from .services import change_priority,change_status,claim_ticket,create_ticket,dispatch_group_tickets,refresh_group_dispatch_status,reply_ticket,revoke_access,share_ticket,transfer_ticket
|
||||
from .services import change_priority,change_status,claim_ticket,create_ticket,dispatch_group_tickets,refresh_group_dispatch_status,reply_ticket,resolve_group_dispatch_tickets,revoke_access,share_ticket,transfer_ticket
|
||||
MESSAGE_PAGE_SIZE=50
|
||||
def conversation_page(queryset,before=None):
|
||||
queryset=queryset.order_by("-created_at","-id")
|
||||
@@ -389,7 +389,7 @@ def staff_customer_search(request):
|
||||
return JsonResponse({"results":results})
|
||||
@staff_required
|
||||
def staff_detail(request,public_id):
|
||||
ticket=get_object_or_404(Ticket.objects.select_related("customer__customer_profile","category","owner__staff_profile","selected_expert__staff_profile"),public_id=public_id)
|
||||
ticket=get_object_or_404(Ticket.objects.select_related("customer__customer_profile","category","owner__staff_profile","selected_expert__staff_profile","group_dispatch__unit","group_dispatch__contact_group"),public_id=public_id)
|
||||
if not TicketPolicy.can_view(request.user,ticket): raise PermissionDenied
|
||||
conversation,has_older,older_cursor=conversation_page(ticket.ticket_messages.select_related("author").prefetch_related("attachments","read_receipts__user__staff_profile","read_receipts__user__customer_profile"))
|
||||
mark_messages_read(conversation,request.user)
|
||||
@@ -397,7 +397,10 @@ def staff_detail(request,public_id):
|
||||
history=Ticket.objects.filter(customer=ticket.customer).exclude(pk=ticket.pk).select_related("owner__staff_profile","selected_expert__staff_profile").order_by("-last_activity_at")[:5]
|
||||
sms_jobs=ticket.notificationjob_set.filter(channel=NotificationJob.Channel.SMS).exclude(event_type="UNIT_SUPERVISOR_NEW_TICKET").order_by("-created_at")
|
||||
latest=conversation[-1] if conversation else None
|
||||
return render(request,"tickets/staff_detail.html",{"ticket":ticket,"conversation":conversation,"has_older":has_older,"older_cursor":older_cursor,"form":StaffReplyForm(),"transfer_form":TransferForm(),"share_form":ShareForm(),"can_claim":TicketPolicy.can_claim(request.user,ticket),"can_transfer":TicketPolicy.can_transfer(request.user,ticket),"can_share":TicketPolicy.can_share(request.user,ticket),"can_delete":TicketPolicy.can_delete(request.user,ticket),"statuses":Ticket.Status.choices,"priorities":Ticket.Priority.choices,"grants":ticket.access_grants.filter(revoked_at__isnull=True).select_related("user__staff_profile"),"customer_history":history,"sms_jobs":sms_jobs,"viewer_user":request.user,"poll_version":conversation_version(ticket,latest),"poll_cursor":latest.public_id if latest else ""})
|
||||
group_dispatch = ticket.group_dispatch
|
||||
can_resolve_dispatch = bool(group_dispatch and DispatchPolicy.can_view(request.user, group_dispatch) and (request.user.has_perm("tickets.change_ticket") or TicketPolicy.has_unit_permission(request.user, group_dispatch.unit, "tickets.change_ticket")))
|
||||
group_open_count = group_dispatch.tickets.exclude(status__in=(Ticket.Status.RESOLVED, Ticket.Status.CLOSED)).count() if group_dispatch else 0
|
||||
return render(request,"tickets/staff_detail.html",{"ticket":ticket,"conversation":conversation,"has_older":has_older,"older_cursor":older_cursor,"form":StaffReplyForm(),"transfer_form":TransferForm(),"share_form":ShareForm(),"can_claim":TicketPolicy.can_claim(request.user,ticket),"can_transfer":TicketPolicy.can_transfer(request.user,ticket),"can_share":TicketPolicy.can_share(request.user,ticket),"can_delete":TicketPolicy.can_delete(request.user,ticket),"can_resolve_dispatch":can_resolve_dispatch,"group_open_count":group_open_count,"statuses":Ticket.Status.choices,"priorities":Ticket.Priority.choices,"grants":ticket.access_grants.filter(revoked_at__isnull=True).select_related("user__staff_profile"),"customer_history":history,"sms_jobs":sms_jobs,"viewer_user":request.user,"poll_version":conversation_version(ticket,latest),"poll_cursor":latest.public_id if latest else ""})
|
||||
@staff_required
|
||||
def staff_messages(request,public_id):
|
||||
ticket=get_object_or_404(Ticket,public_id=public_id)
|
||||
@@ -612,7 +615,24 @@ def staff_dispatch_detail(request, public_id):
|
||||
pending = sum(sms_counts.get(status, 0) for status in (NotificationJob.Status.PENDING, NotificationJob.Status.PROCESSING, NotificationJob.Status.RETRYING))
|
||||
delivery_percent = round((delivered / dispatch.recipient_count) * 100) if dispatch.recipient_count else 0
|
||||
metrics = {"sent": sent, "delivered": delivered, "failed": failed, "retrying": retrying, "tracking": tracking, "pending": pending, "opened": opened, "not_opened": max(dispatch.recipient_count-opened,0), "total_views": total_views, "replied": replied, "delivery_percent": delivery_percent}
|
||||
return render(request, "tickets/staff_dispatch_detail.html", {"dispatch": dispatch, "tickets": tickets, "metrics": metrics, "sms_max_attempts": 1 + settings.SMS_MAX_RETRIES, "active_staff_section": "dispatches"})
|
||||
open_ticket_count = sum(ticket.status not in (Ticket.Status.RESOLVED, Ticket.Status.CLOSED) for ticket in tickets)
|
||||
can_resolve_dispatch = request.user.has_perm("tickets.change_ticket") or TicketPolicy.has_unit_permission(request.user, dispatch.unit, "tickets.change_ticket")
|
||||
return render(request, "tickets/staff_dispatch_detail.html", {"dispatch": dispatch, "tickets": tickets, "metrics": metrics, "open_ticket_count": open_ticket_count, "can_resolve_dispatch": can_resolve_dispatch, "sms_max_attempts": 1 + settings.SMS_MAX_RETRIES, "active_staff_section": "dispatches"})
|
||||
|
||||
|
||||
@staff_required
|
||||
@require_POST
|
||||
def staff_dispatch_resolve(request, public_id):
|
||||
dispatch = get_object_or_404(GroupDispatch, public_id=public_id)
|
||||
try:
|
||||
count = resolve_group_dispatch_tickets(dispatch=dispatch, user=request.user)
|
||||
messages.success(request, f"{count} تیکت این ارسال گروهی حلشده علامتگذاری شد." if count else "همه تیکتهای این ارسال گروهی از قبل حل یا بسته شدهاند.")
|
||||
except PermissionDenied as exc:
|
||||
messages.error(request, str(exc))
|
||||
return_ticket = request.POST.get("return_ticket", "").strip()
|
||||
if return_ticket and dispatch.tickets.filter(public_id=return_ticket).exists():
|
||||
return redirect("staff_tickets:detail", public_id=return_ticket)
|
||||
return redirect("staff_tickets:dispatch_detail", public_id=public_id)
|
||||
|
||||
|
||||
@staff_required
|
||||
|
||||
Reference in new issue
Block a user