first
This commit is contained in:
commit
fa9d154db9
260 files changed
+17643
No files matched your search
@@ -0,0 +1,10 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block header_logo %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="auth-shell">
|
||||
{% if site_branding.login_intro_enabled %}<section class="auth-intro"><div class="auth-intro-icon">{{ site_branding.login_intro_icon }}</div><p class="eyebrow">{{ site_branding.login_intro_eyebrow }}</p><h2>{{ site_branding.login_intro_heading|linebreaksbr }}</h2><p>{{ site_branding.login_intro_body|linebreaksbr }}</p></section>{% endif %}
|
||||
<div class="auth-panel"><section class="auth card">{% include 'includes/auth_brand.html' with hide_copy=True %}<p class="eyebrow">ورود مشتریان</p><h1>خوش آمدید</h1><p class="muted">شماره موبایل خود را وارد کنید؛ نیازی به ثبتنام یا رمز عبور نیست.</p><form method="post" data-disable-on-submit>{% csrf_token %}{{ form.as_p }}<button type="submit">دریافت کد یکبارمصرف <span aria-hidden="true">←</span></button></form><div class="auth-links"><a href="{% url 'accounts:password_login' %}">ورود با رمز عبور</a><a href="{% url 'accounts:staff_login' %}">ورود کارشناسان</a></div></section>{% include 'includes/auth_footer.html' %}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block footer %}{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block header_logo %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="auth-shell">
|
||||
<section class="auth-intro"><div class="auth-intro-icon">●</div><p class="eyebrow">ورود سریع</p><h2>دوباره خوش آمدید.</h2><p>اگر قبلاً در پروفایل خود رمز تعیین کردهاید، بدون انتظار برای پیامک وارد شوید.</p></section>
|
||||
<div class="auth-panel"><section class="auth card">{% include 'includes/auth_brand.html' %}<p class="eyebrow">حساب مشتری</p><h1>ورود با رمز عبور</h1><p class="muted">شماره موبایل و رمز انتخابی خود را وارد کنید.</p><form method="post" data-disable-on-submit>{% csrf_token %}{{ form.as_p }}<button type="submit">ورود به حساب</button></form><div class="auth-links"><a href="{% url 'accounts:login' %}">ورود با کد یکبارمصرف</a></div></section>{% include 'includes/auth_footer.html' %}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block footer %}{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block header_logo %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="auth-shell">
|
||||
<section class="auth-intro"><div class="auth-intro-icon">✓</div><p class="eyebrow">ورود امن</p><h2>فقط یک قدم<br>تا میز کار شما.</h2><p>کد یکبارمصرف کوتاهمدت است و بعد از ورود قابل استفاده مجدد نخواهد بود.</p></section>
|
||||
<div class="auth-panel"><section class="auth card">{% include 'includes/auth_brand.html' %}<p class="eyebrow">تأیید شماره</p><h1>کد را وارد کنید</h1><p class="muted">کد ارسالشده به <b dir="ltr">{{ mobile }}</b> را وارد کنید.</p><form method="post" data-disable-on-submit data-webotp-form>{% csrf_token %}{{ form.as_p }}<button type="submit">تأیید و ورود</button></form><div class="auth-links"><a href="{% url 'accounts:login' %}">اصلاح شماره موبایل</a></div></section>{% include 'includes/auth_footer.html' %}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block footer %}{% endblock %}
|
||||
{% block otp_script %}{% load static %}<script defer src="{% static 'js/webotp.js' %}?v={{ static_asset_version }}"></script>{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends 'base.html' %}{% load static %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-head"><div><p class="eyebrow">تنظیمات حساب</p><h1>پروفایل من</h1><p>اطلاعات شما به کارشناس کمک میکند پاسخ دقیقتری ارائه کند.</p></div><a class="nav-link" href="{% url 'tickets:list' %}">بازگشت به تیکتها</a></div>
|
||||
<section class="card form-card">
|
||||
<div class="badge" dir="ltr">{{ user.mobile }}</div>
|
||||
<form method="post" class="mt-6" data-disable-on-submit>{% csrf_token %}{{ form.non_field_errors }}
|
||||
<div class="grid gap-4 sm:grid-cols-2"><div class="field">{{ form.first_name.label_tag }}{{ form.first_name }}{{ form.first_name.errors }}</div><div class="field">{{ form.last_name.label_tag }}{{ form.last_name }}{{ form.last_name.errors }}</div></div>
|
||||
<div class="field">{{ form.customer_type.label_tag }}{{ form.customer_type }}{{ form.customer_type.errors }}</div>
|
||||
<section class="location-fields" data-location-picker data-locations-url="{% static 'data/iran-locations.json' %}">
|
||||
<div class="location-heading"><div><h3>موقعیت پیشفرض</h3><p>اختیاری است؛ در تیکتهای جدید بهصورت خودکار پیشنهاد میشود.</p></div><span>اختیاری</span></div>
|
||||
<div class="grid gap-4 sm:grid-cols-2"><div class="field">{{ form.province.label_tag }}{{ form.province }}<datalist id="profile-province-options" data-province-options></datalist>{{ form.province.errors }}</div><div class="field">{{ form.city.label_tag }}{{ form.city }}<datalist id="profile-city-options" data-city-options></datalist>{{ form.city.errors }}</div></div>
|
||||
<p class="location-hint" data-location-hint>نام استان را جستوجو و انتخاب کنید.</p>
|
||||
</section>
|
||||
<div class="field">{{ form.password.label_tag }}{{ form.password }}{% if form.password.help_text %}<small class="muted">{{ form.password.help_text }}</small>{% endif %}{{ form.password.errors }}</div>
|
||||
<button type="submit">ذخیره تغییرات</button>
|
||||
</form>
|
||||
</section>
|
||||
<script src="{% static 'js/location-picker.js' %}?v={{ static_asset_version }}"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block header_logo %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="auth-shell">
|
||||
{% if site_branding.staff_login_intro_enabled %}<section class="auth-intro"><div class="auth-intro-icon">{{ site_branding.staff_login_intro_icon }}</div><p class="eyebrow">{{ site_branding.staff_login_intro_eyebrow }}</p><h2>{{ site_branding.staff_login_intro_heading|linebreaksbr }}</h2><p>{{ site_branding.staff_login_intro_body|linebreaksbr }}</p></section>{% endif %}
|
||||
<div class="auth-panel"><section class="auth card">{% include 'includes/auth_brand.html' %}<p class="eyebrow">دسترسی سازمانی</p><h1>ورود کارشناسان</h1><p class="muted">با نام کاربری و رمز عبور سازمانی وارد شوید.</p><form method="post" data-disable-on-submit>{% csrf_token %}{{ form.as_p }}<button type="submit">ورود به میز کار <span aria-hidden="true">←</span></button></form><div class="auth-links"><a href="{% url 'accounts:login' %}">ورود مشتریان</a></div></section>{% include 'includes/auth_footer.html' %}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block footer %}{% endblock %}
|
||||
@@ -0,0 +1,70 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="fa" dir="rtl" data-static-version="{{ static_asset_version }}">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#08376b">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="{{ site_branding.pwa_title }}">
|
||||
<title>{% block title %}{{ page_title }}{% endblock %}</title>
|
||||
<link rel="icon" href="{{ favicon_url }}?v={{ static_asset_version }}-{{ branding_version }}">
|
||||
<link rel="manifest" href="{% url 'web_manifest' %}?v={{ static_asset_version }}-{{ branding_version }}">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ pwa_icon_url }}?v={{ static_asset_version }}-{{ branding_version }}">
|
||||
<link rel="preload" href="{% static 'fonts/vazirmatn-arabic-wght-normal.woff2' %}?v={{ static_asset_version }}" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="{% static 'css/tailwind.css' %}?v={{ static_asset_version }}">
|
||||
<link rel="stylesheet" href="{% static 'css/operations.css' %}?v={{ static_asset_version }}">
|
||||
<link rel="stylesheet" href="{% static 'css/dispatches.css' %}?v={{ static_asset_version }}">
|
||||
<script defer src="{% static 'vendor/htmx-2.0.3.min.js' %}?v={{ static_asset_version }}"></script><script defer src="{% static 'vendor/alpine-3.14.8.min.js' %}?v={{ static_asset_version }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
{% if user.is_authenticated and user.account_type == 'STAFF' %}
|
||||
<div class="staff-shell">
|
||||
<aside class="staff-sidebar">
|
||||
<div>
|
||||
<a class="staff-brand" href="{% url 'staff_tickets:list' %}">
|
||||
<span class="staff-logo-panel"><img class="staff-company-logo" src="{{ company_logo_url }}?v={{ static_asset_version }}-{{ branding_version }}" alt="لوگوی شرکت" width="300" height="130"></span>
|
||||
<span class="staff-brand-copy"><b>{{ site_branding.staff_brand_title }}</b><small>{{ site_branding.staff_brand_subtitle }}</small></span>
|
||||
</a>
|
||||
<p class="staff-menu-label">فضای کاری</p>
|
||||
<nav class="staff-menu" aria-label="منوی کارشناسان">
|
||||
<a class="{% if request.resolver_match.url_name == 'list' and active_tab == '' %}active{% endif %}" href="{% url 'staff_tickets:list' %}" {% if request.resolver_match.url_name == 'list' and active_tab == '' %}aria-current="page"{% endif %}><span>⌂</span><b>میز کار</b></a>
|
||||
<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>
|
||||
{% 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.templates %}<a class="{% if active_staff_section == 'templates' %}active{% endif %}" href="{% url 'staff_tickets:sms_templates' %}" {% if active_staff_section == 'templates' %}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 %}
|
||||
</nav>
|
||||
</div>
|
||||
<div><div class="staff-profile"><span class="staff-profile-avatar">{{ user.staff_profile.display_name|default:user.username|slice:':1' }}</span><div><strong>{{ user.staff_profile.display_name|default:user.username }}</strong><span>{{ user.staff_profile.job_title|default:'کارشناس فروش' }}</span></div></div><div class="staff-developer-credit">{{ staff_sidebar_credit }}</div></div>
|
||||
</aside>
|
||||
<div class="staff-main">
|
||||
<header class="staff-topbar"><div><small>{{ site_branding.staff_panel_label }}</small><h2>{% if active_tab_title %}{{ active_tab_title }}{% else %}{{ site_branding.staff_brand_subtitle }}{% endif %}</h2></div><div class="staff-topbar-actions"><span class="hidden text-xs text-ink/40 sm:block">{{ user.username }}</span><form method="post" action="{% url 'accounts:staff_logout' %}">{% csrf_token %}<button class="logout-button" type="submit">خروج</button></form></div></header>
|
||||
{% else %}
|
||||
<header class="site-header"><div class="header-inner"><a class="logo" href="{% if user.is_authenticated %}{% url 'tickets:list' %}{% else %}{% url 'accounts:login' %}{% endif %}">{% block header_logo %}<img class="header-company-logo" src="{{ company_logo_url }}?v={{ static_asset_version }}-{{ branding_version }}" alt="لوگوی شرکت" width="300" height="130">{% endblock %}<span class="logo-copy">{{ site_branding.header_title }}<small>{{ site_branding.header_subtitle }}</small></span></a>{% if user.is_authenticated %}<nav class="desktop-nav"><a href="{% url 'tickets:list' %}">درخواستهای من</a><a href="{% url 'accounts:profile' %}">پروفایل</a><form method="post" action="{% url 'accounts:logout' %}">{% csrf_token %}<button class="logout-button" type="submit">خروج</button></form></nav>{% endif %}</div></header>
|
||||
{% endif %}
|
||||
|
||||
<main class="{% if user.is_authenticated and user.account_type == 'STAFF' %}staff-content{% else %}customer-main{% endif %}">{% for message in messages %}<div class="flash {{ message.tags }}" role="status">{{ message }}</div>{% endfor %}{% block content %}{% endblock %}</main>
|
||||
{% block footer %}<footer class="site-footer">{{ developer_credit }}</footer>{% endblock %}
|
||||
|
||||
{% if user.is_authenticated and user.account_type == 'STAFF' %}
|
||||
</div>
|
||||
<nav class="staff-mobile-dock" aria-label="دسترسی سریع کارشناسان">
|
||||
<a class="{% if request.resolver_match.url_name == 'list' and active_tab == '' %}active{% endif %}" href="{% url 'staff_tickets:list' %}" {% if request.resolver_match.url_name == 'list' and active_tab == '' %}aria-current="page"{% endif %}><span class="dock-icon">⌂</span>میز کار</a>
|
||||
<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>
|
||||
{% 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>
|
||||
</div>
|
||||
{% elif user.is_authenticated %}
|
||||
<nav class="mobile-dock customer-mobile-dock" aria-label="دسترسی سریع مشتری"><a class="{% if request.resolver_match.namespace == 'tickets' and request.resolver_match.url_name != 'new' %}active{% endif %}" href="{% url 'tickets:list' %}" {% if request.resolver_match.namespace == 'tickets' and request.resolver_match.url_name != 'new' %}aria-current="page"{% endif %}><span class="dock-icon">◎</span>درخواستها</a><a class="{% if request.resolver_match.url_name == 'new' %}active{% endif %}" href="{% url 'tickets:new' %}" {% if request.resolver_match.url_name == 'new' %}aria-current="page"{% endif %}><span class="dock-icon">+</span>درخواست جدید</a><a class="{% if request.resolver_match.namespace == 'accounts' and request.resolver_match.url_name == 'profile' %}active{% endif %}" href="{% url 'accounts:profile' %}" {% if request.resolver_match.namespace == 'accounts' and request.resolver_match.url_name == 'profile' %}aria-current="page"{% endif %}><span class="dock-icon">○</span>پروفایل</a><form method="post" action="{% url 'accounts:logout' %}">{% csrf_token %}<button type="submit"><span class="dock-icon">←</span>خروج</button></form></nav>
|
||||
{% endif %}
|
||||
{% if user.is_authenticated %}<aside class="pwa-install" data-pwa-install hidden><img class="pwa-install-icon" src="{{ pwa_icon_url }}?v={{ static_asset_version }}-{{ branding_version }}" alt="" width="64" height="64"><div class="pwa-install-copy"><b>{{ site_branding.pwa_install_label }}</b><small data-pwa-help role="status">{{ site_branding.pwa_install_help }}</small></div><div class="pwa-install-actions"><button type="button" data-pwa-dismiss>بعداً</button><button type="button" class="primary" data-pwa-accept>نصب</button></div></aside>{% endif %}
|
||||
<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 %}
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
{% block otp_script %}{% endblock %}
|
||||
</body></html>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="auth-brand" aria-label="{{ site_branding.header_title }}">
|
||||
<img src="{{ company_logo_url }}?v={{ static_asset_version }}-{{ branding_version }}" alt="لوگوی شرکت" width="300" height="130">
|
||||
{% if not hide_copy %}<div><strong>{{ site_branding.header_title }}</strong><small>{{ site_branding.header_subtitle }}</small></div>{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<div class="auth-footer">{{ developer_credit }}</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends 'base.html' %}{% load jalali %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-head"><div><p class="eyebrow">ردپای تغییرات</p><h1>تاریخچه ویرایش</h1><p>نسخههای قبلی پیام بهترتیب زمان ثبت نمایش داده میشوند.</p></div><a class="nav-link" href="javascript:history.back()">بازگشت</a></div>
|
||||
<div class="grid gap-3">
|
||||
{% for rev in revisions %}
|
||||
<article class="card"><time class="text-xs text-slate-400">{{ rev.created_at|jalali_datetime }}</time><p class="mt-3 leading-8">{{ rev.body|linebreaksbr }}</p></article>
|
||||
{% empty %}
|
||||
<div class="empty">نسخه قبلی برای این پیام وجود ندارد.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% load static %}
|
||||
const CACHE='sales-ticket-{{ static_asset_version }}';
|
||||
const SHELL=['{% static "fonts/vazirmatn-arabic-wght-normal.woff2" %}?v={{ static_asset_version }}','{{ company_logo_url }}?v={{ static_asset_version }}-{{ branding_version }}','{{ favicon_url }}?v={{ static_asset_version }}-{{ branding_version }}','{{ pwa_icon_url }}?v={{ static_asset_version }}-{{ branding_version }}'];
|
||||
self.addEventListener('install',event=>event.waitUntil(caches.open(CACHE).then(cache=>cache.addAll(SHELL)).then(()=>self.skipWaiting())));
|
||||
self.addEventListener('activate',event=>event.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(key=>key!==CACHE).map(key=>caches.delete(key)))).then(()=>self.clients.claim())));
|
||||
self.addEventListener('fetch',event=>{if(event.request.method!=='GET'||new URL(event.request.url).origin!==location.origin)return;const url=new URL(event.request.url);if(event.request.mode==='navigate'){event.respondWith(fetch(event.request,{cache:'no-store'}).catch(()=>new Response('برای مشاهده تیکتها به اینترنت متصل شوید.',{status:503,headers:{'Content-Type':'text/plain; charset=utf-8'}})));return}const isStatic=url.pathname.startsWith('/static/');const isAvatar=url.pathname.includes('/experts/')&&url.pathname.endsWith('/avatar/');if(!isStatic&&!isAvatar){event.respondWith(fetch(event.request,{cache:'no-store'}));return}const mutable=event.request.destination==='style'||event.request.destination==='script'||isAvatar;if(mutable){event.respondWith(fetch(event.request,{cache:'no-store'}).then(response=>{const copy=response.clone();caches.open(CACHE).then(cache=>cache.put(event.request,copy));return response}).catch(()=>caches.match(event.request)));return}event.respondWith(caches.match(event.request).then(cached=>cached||fetch(event.request).then(response=>{const copy=response.clone();caches.open(CACHE).then(cache=>cache.put(event.request,copy));return response}))) });
|
||||
@@ -0,0 +1,5 @@
|
||||
<dialog class="image-viewer" data-image-viewer aria-label="نمایش تصویر پیوست">
|
||||
<header><b data-image-title>تصویر پیوست</b><button type="button" data-image-close aria-label="بستن">×</button></header>
|
||||
<div class="image-viewer-stage"><img data-image-preview alt="تصویر پیوست"></div>
|
||||
<footer><a class="button secondary" data-image-download download>⇩ دانلود تصویر</a></footer>
|
||||
</dialog>
|
||||
@@ -0,0 +1,22 @@
|
||||
{% 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 %}<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="{% url 'attachments:view' file.public_id %}" data-image-name="{{ file.original_name }}"><img src="{% url 'attachments:view' file.public_id %}" alt="{{ file.original_name }}"><span>{{ file.original_name }} · {{ file.file_size|filesizeformat }}</span></button><a class="attachment-download" href="{% url 'attachments:download' file.public_id %}" download>⇩ دانلود تصویر</a></div>
|
||||
{% elif file.attachment_type == 'VOICE' %}<div class="attachment attachment-audio"><span>🎙 پیام صوتی</span><audio controls preload="metadata" src="{% url 'attachments:view' file.public_id %}"></audio><a href="{% url 'attachments:download' file.public_id %}">دریافت فایل</a></div>
|
||||
{% else %}<a class="attachment attachment-file" href="{% url 'attachments:download' file.public_id %}"><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 %}
|
||||
{% 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 %}
|
||||
{% if item.author == user and not item.deleted_at %}<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 %}
|
||||
</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 older_route ticket.public_id %}?before={{ older_cursor }}" hx-target="this" hx-swap="outerHTML">↑ نمایش مکاتبات قدیمیتر</button>{% endif %}
|
||||
{% include 'tickets/_messages.html' %}
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="customer-result-head"><span>{{ customers|length }} مشتری</span>{% if search %}<small>نتیجه برای «{{ search }}»</small>{% else %}<small>مرتبشده بر اساس تیکتهای فعال</small>{% endif %}</div>
|
||||
<div class="customer-grid">
|
||||
{% for customer in customers %}{% with profile=customer.customer_profile %}<article class="customer-card"><header><span class="customer-avatar">{{ profile.first_name|default:customer.local_mobile|slice:':1' }}</span><div><h2>{{ profile|default:customer.local_mobile }}</h2><a dir="ltr" href="tel:{{ customer.mobile }}">{{ customer.local_mobile }}</a></div></header><p class="customer-location">{% if profile.city or profile.province %}{{ profile.city }}{% if profile.city and profile.province %}، {% endif %}{{ profile.province }}{% else %}موقعیت ثبت نشده{% endif %}</p><div class="customer-ticket-stats"><span><b>{{ customer.ticket_count }}</b>کل تیکت</span><span><b>{{ customer.active_count }}</b>فعال</span><span class="needs-action"><b>{{ customer.waiting_staff_count }}</b>نیازمند اقدام</span><span><b>{{ customer.resolved_count }}</b>مختومه</span></div>{% if perms.tickets.add_ticket %}<a class="customer-new-ticket" href="{% url 'staff_tickets:create' %}?customer={{ customer.public_id }}">ثبت تیکت برای این مشتری</a>{% endif %}</article>{% endwith %}
|
||||
{% empty %}<div class="empty-state"><h2>مشتری پیدا نشد</h2><p>نام یا شماره موبایل را بررسی کنید؛ برای شماره جدید میتوانید مستقیماً تیکت بسازید.</p>{% if perms.tickets.add_ticket %}<a class="button" href="{% url 'staff_tickets:create' %}">ثبت مشتری و تیکت جدید</a>{% endif %}</div>{% endfor %}
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
{% load jalali %}<div class="staff-list">
|
||||
{% for ticket in tickets %}
|
||||
<a class="staff-ticket priority-edge-{{ ticket.priority|lower }} status-row-{{ ticket.status|lower }}" href="{% url 'staff_tickets:detail' ticket.public_id %}">
|
||||
<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 %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="staff-ticket-side"><div class="staff-ticket-badges"><span class="badge priority-{{ ticket.priority|lower }}">{{ ticket.get_priority_display }}</span><span class="badge status-{{ ticket.status|lower }}">{{ ticket.get_status_display }}</span></div><span class="ticket-owner">{{ ticket.assigned_expert_name }}</span><span class="ticket-open">مشاهده <b>←</b></span></div>
|
||||
</a>
|
||||
{% empty %}
|
||||
<div class="empty"><div class="empty-icon">✓</div><h2>درخواستی در این صف نیست</h2><p class="muted">فیلترها را تغییر دهید یا یکی از صفهای دیگر را بررسی کنید.</p><a class="button secondary mt-4" href="{% url 'staff_tickets:list' %}">بازگشت به میز کار</a></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}{% load static jalali %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}
|
||||
{% if magic_link_access %}<div class="flash" role="status">شما از طریق لینک امن، فقط به همین تیکت دسترسی دارید. برای مشاهده سایر درخواستها و پروفایل، با شماره موبایل خود وارد شوید.</div>{% endif %}
|
||||
<div class="ticket-workspace customer-workspace" data-conversation-workspace data-poll-url="{% url 'tickets:poll' ticket.public_id %}" data-read-url="{% url 'tickets:read' ticket.public_id %}" data-poll-version="{{ poll_version }}" data-poll-cursor="{{ poll_cursor }}" data-draft-key="ticket-{{ ticket.public_id }}-customer" data-reply-result="{{ request.GET.reply_result|default:'' }}">
|
||||
<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" 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>
|
||||
<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>
|
||||
<form method="post" enctype="multipart/form-data" data-ticket-composer data-chat-coach data-chat-coach-key="ticket-{{ ticket.public_id }}-customer" action="{% url 'tickets:reply' ticket.public_id %}">{% csrf_token %}
|
||||
<div class="composer-context"><div><b>ثبت پاسخ تکمیلی در تیکت</b><small>لطفاً توضیحات خود را کامل، دقیق و یکجا بنویسید تا کارشناس بتواند پاسخ مناسبی ارائه کند.</small></div><div class="composer-context-actions"><span data-draft-status></span><span class="composer-send-icon" role="button" tabindex="0" data-scroll-to-submit aria-label="رفتن به دکمه ثبت پاسخ" title="رفتن به دکمه ثبت پاسخ"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 20.5 21 12 3 3.5v6.6l12 1.9-12 1.9z"/></svg></span></div></div>
|
||||
{{ form.body }}{{ form.body.errors }}<div class="reply-media-picker"><div class="media-picker-actions"><label class="media-picker-button gallery-button" for="customer-reply-gallery">▣ انتخاب عکس از گالری</label><label class="media-picker-button" for="customer-reply-file">+ انتخاب فایل</label></div><input id="customer-reply-gallery" type="file" accept="image/*" multiple data-gallery-input><input id="customer-reply-file" 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>
|
||||
<div class="file-preview" data-file-preview></div>
|
||||
<div class="voice-recorder compact-recorder" data-voice-recorder hidden><div class="voice-head"><b>ضبط پیام صوتی</b><span class="badge" data-timer>00:00</span></div><div class="voice-actions"><button type="button" data-record>● شروع ضبط</button><button type="button" data-pause disabled>مکث</button><button type="button" data-stop disabled>توقف</button><button type="button" class="ghost" data-delete hidden>حذف</button></div><audio controls hidden></audio></div>
|
||||
<div class="composer-bar"><div class="composer-tools"><button class="tool-button" type="button" data-toggle-recorder title="افزودن توضیح صوتی">●<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>←</span></button></div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<aside class="workspace-side" id="ticket-info" data-workspace-panel><button type="button" class="panel-close" data-close-panel aria-label="بستن">×</button><section class="info-card"><p class="panel-index">جزئیات تیکت</p><div class="info-list"><div class="info-row"><span>دستهبندی</span><b>{{ ticket.category }}</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.assigned_expert_name }}</b></div><div class="info-row"><span>آخرین فعالیت</span><b>{{ ticket.last_activity_at|jalali_datetime }}</b></div></div></section><section class="info-card">{% if magic_link_access %}<h3>مشاهده همه درخواستها</h3><p class="muted mt-2">برای دسترسی به پروفایل و سایر درخواستها، ابتدا با شماره موبایل خود وارد شوید.</p><a class="back-link mt-3" href="{% url 'accounts:login' %}">ورود با شماره موبایل ←</a>{% else %}<h3>موضوع دیگری دارید؟</h3><p class="muted mt-2">برای هر موضوع یک درخواست جدا بسازید تا پیگیری منظم بماند.</p><a class="back-link mt-3" href="{% url 'tickets:new' %}">ثبت درخواست تازه ←</a>{% endif %}</section></aside>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'tickets/_image_viewer.html' %}
|
||||
<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>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}{% load jalali %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}
|
||||
{% if site_branding.customer_tickets_hero_enabled %}<section class="hero-panel">
|
||||
<p class="page-kicker">{{ site_branding.customer_tickets_hero_eyebrow }}</p>
|
||||
<h1>{{ site_branding.customer_tickets_hero_heading|linebreaksbr }}</h1>
|
||||
<p>{{ site_branding.customer_tickets_hero_body|linebreaksbr }}</p>
|
||||
<div class="hero-actions">
|
||||
<a class="button" href="{% url 'tickets:new' %}"><span>+</span> ثبت درخواست جدید</a>
|
||||
<a class="button hero-history-button" href="#recent-requests"><span aria-hidden="true">↓</span> درخواستهای قبلی</a>
|
||||
</div>
|
||||
</section>{% endif %}
|
||||
{% if missing_profile_fields %}<aside class="profile-completion-notice" role="note"><span class="profile-completion-icon" aria-hidden="true">◎</span><div><h2>تکمیل پروفایل، برای پیگیری دقیقتر</h2><p>مشتری گرامی، اطلاعات <b>{{ missing_profile_fields|join:'، ' }}</b> در پروفایل شما تکمیل نشده است. ثبت این اطلاعات اختیاری است؛ بااینحال توصیه میکنیم پیش از ثبت درخواست، آنها را وارد کنید تا کارشناسان فروش بتوانند دقیقتر و سریعتر راهنماییتان کنند.</p><div class="profile-completion-actions"><a class="button" href="{% url 'accounts:profile' %}">تکمیل پروفایل</a><a href="{% url 'tickets:new' %}">فعلاً ثبت درخواست را ادامه میدهم</a></div></div></aside>{% endif %}
|
||||
<div class="page-head" id="recent-requests">
|
||||
<div><p class="page-kicker">پیگیریها</p><h2>درخواستهای اخیر شما</h2></div>
|
||||
<span class="muted">{{ tickets|length }} مورد</span>
|
||||
</div>
|
||||
<div class="ticket-grid">
|
||||
{% for ticket in tickets %}
|
||||
<a class="ticket-card" href="{% url 'tickets:detail' ticket.public_id %}">
|
||||
<div><div class="ticket-card-top"><span class="ticket-number" dir="ltr">{{ ticket.ticket_number }}</span><span class="badge">{{ ticket.get_status_display }}</span></div><h2>{{ ticket.subject }}</h2></div>
|
||||
<div class="ticket-meta"><div><small>{{ ticket.category }}</small><div class="expert-line">{{ ticket.assigned_expert_name }}</div></div><small>{{ ticket.last_activity_at|jalali_datetime }}</small></div>
|
||||
</a>
|
||||
{% empty %}
|
||||
<div class="empty"><div class="empty-icon">✦</div><h2>هنوز درخواستی ثبت نشده</h2><p class="muted">اولین گفتوگوی خود را با تیم فروش آغاز کنید.</p><a class="button" href="{% url 'tickets:new' %}">ثبت اولین درخواست</a></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends 'base.html' %}{% load static %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}
|
||||
<link rel="stylesheet" href="{% static 'css/ticket-composer.css' %}?v={{ static_asset_version }}">
|
||||
<link rel="stylesheet" href="{% static 'css/ticket-composer-v2.css' %}?v={{ static_asset_version }}">
|
||||
<div class="page-head"><div><a class="back-link" href="{% url 'tickets:list' %}">→ بازگشت به درخواستها</a><p class="page-kicker">ثبت درخواست جدید</p><h1>چه کمکی از ما برمیآید؟</h1><p>درخواست خود را کامل بنویسید تا همان بار اول پاسخ دقیقتری دریافت کنید.</p></div></div>
|
||||
<div class="request-layout">
|
||||
<section class="request-form card"><form method="post" enctype="multipart/form-data" data-ticket-composer data-unit-options-url="{% url 'tickets:new_options' %}" data-ticket-search-url="{% url 'tickets:new_search' %}" data-chat-coach data-chat-coach-key="customer-new-{{ user.public_id }}">{% csrf_token %}{{ form.non_field_errors }}
|
||||
<section class="intent-finder" aria-labelledby="intent-finder-title"><span class="intent-icon">⌕</span><div class="intent-copy"><h2 id="intent-finder-title">موضوع درخواستتان چیست؟</h2><p>چند کلمه بنویسید؛ واحد و دستهبندی مناسب را پیشنهاد میدهیم.</p><label class="intent-search"><span class="sr-only">جستوجوی موضوع درخواست</span><input type="search" data-ticket-search autocomplete="off" placeholder="مثلاً فیش واریزی، موجودی کالا یا ارسال بار..."><kbd>جستوجو</kbd></label><div class="intent-results" data-ticket-search-results aria-live="polite"></div></div></section>
|
||||
<details class="manual-routing" open><summary><span>انتخاب دستی واحد و دستهبندی</span><small>در صورت تمایل خودتان مسیر درخواست را مشخص کنید</small></summary><div class="manual-routing-body">
|
||||
<div class="step-title"><span class="step-number">۱</span><div><h3>موضوع درخواست</h3><p class="muted">دستهبندی و یک عنوان کوتاه انتخاب کنید.</p></div></div>
|
||||
<div class="routing-selectors" data-routing-selectors><div class="field">{{ form.unit.label_tag }}{{ form.unit }}{{ form.unit.errors }}</div><div class="field">{{ form.category.label_tag }}{{ form.category }}{{ form.category.errors }}</div></div>
|
||||
<div class="field routing-subject">{{ form.subject.label_tag }}{{ form.subject }}{{ form.subject.errors }}<small>موضوع را بهصورت دستی و دقیق وارد کنید.</small></div>
|
||||
</div></details>
|
||||
<div class="step-title"><span class="step-number">۲</span><div><h3>موقعیت این درخواست</h3><p class="muted">اختیاری است؛ موقعیت ذخیرهشده پروفایل از قبل انتخاب شده و فقط در صورت نیاز آن را تغییر دهید.</p></div></div>
|
||||
<section class="location-fields" data-location-picker data-locations-url="{% static 'data/iran-locations.json' %}">
|
||||
<div class="grid gap-4 sm:grid-cols-2"><div class="field">{{ form.province.label_tag }}{{ form.province }}{{ form.province.errors }}</div><div class="field">{{ form.city.label_tag }}{{ form.city }}{{ form.city.errors }}</div></div>
|
||||
<p class="location-hint" data-location-hint>نام استان را جستوجو و انتخاب کنید.</p>
|
||||
</section>
|
||||
<div class="step-title"><span class="step-number">۳</span><div><h3>انتخاب کارشناس</h3><p class="muted">اختیاری است؛ صف عمومی معمولاً سریعترین انتخاب است.</p></div></div>
|
||||
<fieldset class="expert-picker" data-expert-picker><legend class="sr-only">کارشناس دلخواه</legend><label class="expert-card" data-general-queue data-unit-id="{{ selected_unit.pk|default:'' }}"><input type="radio" name="selected_expert" value="" {% if not form.selected_expert.value %}checked{% endif %}><span class="expert-avatar">⌁</span><strong>صف عمومی {{ selected_unit.name|default:'واحد' }}</strong><small>اولین کارشناس آزاد همین واحد</small></label>{% for expert in form.fields.selected_expert.queryset %}<label class="expert-card"><input type="radio" name="selected_expert" value="{{ expert.pk }}" {% if form.selected_expert.value|stringformat:'s' == expert.pk|stringformat:'s' %}checked{% endif %}><img src="{% url 'tickets:expert_avatar' expert.user.public_id %}?v={{ expert.updated_at|date:'U' }}" alt="تصویر {{ expert.display_name }}" width="56" height="56" loading="lazy"><strong>{{ expert.display_name }}</strong><small>{{ expert.job_title|default:'کارشناس واحد' }}</small></label>{% empty %}<p class="muted expert-empty">کارشناس قابل انتخابی ثبت نشده؛ درخواست به صف عمومی میرود.</p>{% endfor %}</fieldset>{{ form.selected_expert.errors }}
|
||||
<div class="step-title"><span class="step-number">۴</span><div><h3>شرح و مستندات</h3><p class="muted">متن، فایل یا پیام صوتی خود را اضافه کنید.</p></div></div>
|
||||
<div class="field">{{ form.body.label_tag }}{{ form.body }}{{ form.body.errors }}</div>
|
||||
<div class="upload-zone media-picker"><div class="media-picker-actions"><label class="media-picker-button gallery-button" for="ticket-gallery-input">▣ انتخاب عکس از گالری</label><label class="media-picker-button" for="ticket-file-input">+ انتخاب فایل</label></div><input id="ticket-gallery-input" type="file" accept="image/*" multiple data-gallery-input><input id="ticket-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></div></div>
|
||||
<div class="voice-recorder" data-voice-recorder><div class="voice-head"><b>پیام صوتی</b><span class="badge" data-timer>00:00</span></div><div class="voice-actions"><button type="button" data-record>● شروع ضبط</button><button type="button" data-pause disabled>مکث</button><button type="button" data-stop disabled>توقف</button><button type="button" class="ghost" data-delete hidden>حذف</button></div><audio class="mt-3 w-full" controls hidden></audio></div>
|
||||
<button class="submit-ticket" type="submit">ثبت و ارسال درخواست <span>←</span></button>
|
||||
</form></section>
|
||||
<aside class="request-aside"><div class="help-card"><h3>برای پاسخ سریعتر</h3><div class="help-list"><div class="help-item"><b>۱</b><span>نام محصول یا خدمت موردنظر را دقیق بنویسید.</span></div><div class="help-item"><b>۲</b><span>تعداد، شهر مقصد و زمان موردنظر را ذکر کنید.</span></div><div class="help-item"><b>۳</b><span>تصویر یا فایل مرتبط را همینجا ضمیمه کنید.</span></div></div></div></aside>
|
||||
</div>
|
||||
<script src="{% static 'vendor/compressor.min.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/location-picker.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/ticket-unit-options.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/ticket-intent-search.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/attachment-uploader.js' %}?v={{ static_asset_version }}"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}<div class="operations-page operations-contacts">
|
||||
<section class="operations-hero"><div><p class="eyebrow">مرکز یکپارچه ارتباطات</p><h1>مخاطبان و مشتریان</h1><p>پرونده ارتباطی مشتریان، تأمینکنندگان و باربریها همراه با گروهها و وضعیت تیکتها در یک نما.</p></div><div class="operations-hero-actions"><a class="button button-secondary" href="{% url 'staff_tickets:groups' %}">مدیریت گروهها</a>{% if can_add_contact %}<a class="button" href="{% url 'staff_tickets:contact_create' %}">+ مخاطب جدید</a>{% endif %}</div></section>
|
||||
<section class="operations-kpis" aria-label="آمار مخاطبان"><article><span class="kpi-icon">◎</span><div><b>{{ metrics.total }}</b><small>نتیجه قابل مشاهده</small></div></article><article><span class="kpi-icon is-blue">♙</span><div><b>{{ metrics.customers }}</b><small>مشتری</small></div></article><article><span class="kpi-icon is-amber">▣</span><div><b>{{ metrics.suppliers }}</b><small>تأمینکننده</small></div></article><article><span class="kpi-icon is-violet">⇄</span><div><b>{{ metrics.carriers }}</b><small>باربری</small></div></article><article><span class="kpi-icon is-coral">!</span><div><b>{{ metrics.needs_action }}</b><small>تیکت نیازمند اقدام</small></div></article></section>
|
||||
<section class="operations-panel"><form method="get" class="contact-filter-bar"><label class="contact-search"><span>⌕</span><input type="search" name="q" value="{{ request.GET.q }}" placeholder="جستوجو با نام، سازمان یا شماره همراه"></label><label><span>نوع مخاطب</span><select name="stakeholder_type"><option value="">همه انواع</option>{% for value,label in stakeholder_types %}<option value="{{ value }}" {% if request.GET.stakeholder_type == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><label><span>گروه</span><select name="group"><option value="">همه گروهها</option>{% for group in groups %}<option value="{{ group.pk }}" {% if request.GET.group == group.pk|stringformat:'s' %}selected{% endif %}>{{ group.name }} ({{ group.contact_count }})</option>{% endfor %}</select></label><label><span>استان</span><select name="province"><option value="">همه استانها</option>{% for province in provinces %}<option value="{{ province }}" {% if request.GET.province == province %}selected{% endif %}>{{ province }}</option>{% endfor %}</select></label><button type="submit">اعمال فیلتر</button>{% if request.GET %}<a class="filter-reset" href="{% url 'staff_tickets:contacts' %}">پاککردن</a>{% endif %}</form>
|
||||
<div class="contact-result-meta"><div><b>{{ contacts|length }} مخاطب</b><small>مرتبشده بر اساس تیکتهای فعال و آخرین ثبت</small></div><a href="{% url 'staff_tickets:groups' %}">{{ groups|length }} گروه قابل دسترس ←</a></div><div class="contact-card-grid">{% for contact in contacts %}{% with profile=contact.customer_profile %}<article class="contact-card"><header><span class="contact-avatar">{{ profile.first_name|default:contact.local_mobile|slice:':1' }}</span><div class="contact-identity"><h2>{{ profile|default:contact.local_mobile }}</h2><p>{{ profile.organization_name|default:profile.get_stakeholder_type_display }}</p></div><span class="contact-type type-{{ profile.stakeholder_type|lower }}">{{ profile.get_stakeholder_type_display }}</span></header><div class="contact-meta"><span dir="ltr">☎ {{ contact.local_mobile }}</span><span>⌖ {% if profile.city or profile.province %}{{ profile.city }}{% if profile.city and profile.province %}، {% endif %}{{ profile.province }}{% else %}موقعیت ثبت نشده{% endif %}</span></div><div class="contact-groups">{% for group in profile.visible_groups %}<span>{{ group.name }}</span>{% empty %}<small>بدون گروه قابل مشاهده</small>{% endfor %}</div><div class="contact-ticket-stats"><span><b>{{ contact.ticket_count }}</b>کل تیکت</span><span><b>{{ contact.active_count }}</b>فعال</span><span class="needs-action"><b>{{ contact.waiting_staff_count }}</b>نیازمند اقدام</span><span><b>{{ contact.resolved_count }}</b>مختومه</span></div><footer><a href="{% url 'staff_tickets:contact_edit' contact.public_id %}">ویرایش پرونده</a>{% if staff_operations_access.create_ticket %}<a class="primary-action" href="{% url 'staff_tickets:create' %}?customer={{ contact.public_id }}">+ ثبت تیکت</a>{% endif %}</footer></article>{% endwith %}{% empty %}<div class="empty-state contact-empty"><span>⌕</span><h2>مخاطبی پیدا نشد</h2><p>فیلترها را تغییر دهید یا مخاطب جدیدی ثبت کنید.</p>{% if can_add_contact %}<a class="button" href="{% url 'staff_tickets:contact_create' %}">ثبت مخاطب جدید</a>{% endif %}</div>{% endfor %}</div></section></div>{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends 'base.html' %}{% load static %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="staff-create-page">
|
||||
<header class="create-page-head"><div><a class="create-back" href="{% url 'staff_tickets:list' %}">→ بازگشت به میز کار</a><p class="eyebrow">ایجاد پرونده از طرف واحد فروش</p><h1>ثبت تیکت برای مشتری</h1><p>ابتدا مشتری را شناسایی کنید، سپس موضوع و پیام کامل را ثبت کنید. مشتری یک لینک امن برای مشاهده دریافت میکند.</p></div><a class="customer-directory-link" href="{% url 'staff_tickets:customers' %}"><span>♙</span><div><b>برنامه مشتریان</b><small>جستوجو و مشاهده سوابق</small></div></a></header>
|
||||
<ol class="create-progress" aria-label="مراحل ثبت تیکت"><li class="active"><span>۱</span><div><b>انتخاب مشتری</b><small>جستوجو یا ثبت شماره جدید</small></div></li><li><span>۲</span><div><b>تعریف درخواست</b><small>دستهبندی و موضوع</small></div></li><li><span>۳</span><div><b>پیام و ارسال</b><small>شرح کامل و پیوستها</small></div></li></ol>
|
||||
<section class="customer-ticket-create" data-customer-picker data-search-url="{% url 'staff_tickets:customer_search' %}" data-location-picker data-locations-url="{% static 'data/iran-locations.json' %}">
|
||||
<form method="post" enctype="multipart/form-data" data-disable-on-submit class="assisted-ticket-form">{% csrf_token %}{{ form.customer_id }}
|
||||
{% if form.non_field_errors %}<div class="create-form-errors" role="alert">{{ form.non_field_errors }}</div>{% endif %}
|
||||
<div class="create-form-main">
|
||||
<section class="create-section customer-section"><header class="create-section-head"><span class="section-number">۱</span><div><h2>مشتری این تیکت کیست؟</h2><p>در بانک مشتریان جستوجو کنید یا شماره موبایل را مستقیماً وارد کنید.</p></div></header>
|
||||
<label class="customer-search-box"><span>جستوجوی نام یا موبایل</span><div class="search-input-shell"><span aria-hidden="true">⌕</span><input type="search" data-customer-search autocomplete="off" placeholder="مثلاً محمد رضایی یا 0912…"></div><small>نتایج پس از واردکردن حداقل دو حرف یا رقم نمایش داده میشوند.</small></label><div class="customer-search-results" data-customer-results hidden></div>
|
||||
<div class="selected-customer" data-selected-customer hidden aria-live="polite"><span class="selected-customer-icon">✓</span><div><b data-selected-name></b><span data-selected-mobile dir="ltr"></span></div><div><span data-selected-location></span><small data-selected-stats></small></div><button type="button" data-clear-customer>تغییر مشتری</button></div><div class="customer-lookup-status" data-customer-status role="status">شماره موبایل را وارد کنید تا عضویت مشتری بررسی شود.</div>
|
||||
<div class="field-grid customer-fields"><label class="field-control"><span>{{ form.customer_mobile.label }} <em>*</em></span>{{ form.customer_mobile }}{{ form.customer_mobile.errors }}<small>شماره به شکل 09xxxxxxxxx</small></label><label class="field-control"><span>{{ form.stakeholder_type.label }}</span>{{ form.stakeholder_type }}{{ form.stakeholder_type.errors }}</label><label class="field-control" data-new-customer-field><span>{{ form.first_name.label }} <em>*</em></span>{{ form.first_name }}{{ form.first_name.errors }}</label><label class="field-control" data-new-customer-field><span>{{ form.last_name.label }} <em>*</em></span>{{ form.last_name }}{{ form.last_name.errors }}</label><label class="field-control"><span>{{ form.organization_name.label }}</span>{{ form.organization_name }}{{ form.organization_name.errors }}</label><label class="field-control"><span>{{ form.groups.label }}</span>{{ form.groups }}{{ form.groups.errors }}<small>{{ form.groups.help_text }}</small></label><label class="field-control"><span>{{ form.province.label }} <i>اختیاری</i></span>{{ form.province }}{{ form.province.errors }}</label><label class="field-control"><span>{{ form.city.label }} <i>اختیاری</i></span>{{ form.city }}{{ form.city.errors }}</label></div><datalist id="ticket-province-options" data-province-options></datalist><datalist id="ticket-city-options" data-city-options></datalist><p class="location-hint" data-location-hint>استان و شهر اختیاری هستند.</p>
|
||||
</section>
|
||||
<section class="create-section"><header class="create-section-head"><span class="section-number">۲</span><div><h2>مالکیت و موضوع درخواست</h2><p>واحد ایجادکننده بهصورت امن از عضویت اصلی شما تعیین میشود.</p></div></header>{{ form.unit }}{{ form.unit.errors }}<div class="field-grid"><div class="field-control"><span>{{ form.unit.label }}</span><strong data-staff-unit data-unit-id="{{ staff_unit.pk|default:'' }}">{{ staff_unit.name|default:'واحد فعالی برای کاربر تعریف نشده' }}</strong><small>این مقدار قابل تغییر نیست.</small></div><label class="field-control"><span>{{ form.category.label }} <em>*</em></span>{{ form.category }}{{ form.category.errors }}</label><label class="field-control"><span>{{ form.subject.label }} <em>*</em></span>{{ form.subject }}{{ form.subject.errors }}<small>نمونه: پیگیری پیشفاکتور لوله پنجلایه</small></label></div></section>
|
||||
<section class="create-section message-section"><header class="create-section-head"><span class="section-number">۳</span><div><h2>پیام کامل کارشناس</h2><p>موضوع، اقدام انجامشده و انتظار از مشتری را یکجا و کامل توضیح دهید.</p></div></header><label class="field-control message-field"><span>{{ form.body.label }} <em>*</em></span>{{ form.body }}{{ form.body.errors }}<small>این متن نخستین پیام رسمی پرونده است و برای مشتری نمایش داده میشود.</small></label><label class="attachment-field"><span class="attachment-icon">+</span><div><b>{{ form.files.label }}</b><small>تصویر، صوت یا سند مرتبط را انتخاب کنید؛ پیوست اختیاری است.</small></div>{{ form.files }}{{ form.files.errors }}</label></section>
|
||||
</div>
|
||||
<aside class="create-summary"><div class="summary-sticky"><header><span>خلاصه پرونده</span><small>پیش از ارسال بررسی کنید</small></header><dl><div><dt>مشتری</dt><dd data-summary-customer>هنوز انتخاب نشده</dd></div><div><dt>موضوع</dt><dd data-summary-subject>هنوز وارد نشده</dd></div><div><dt>دستهبندی</dt><dd data-summary-category>انتخاب نشده</dd></div></dl><div class="secure-delivery"><span>⌁</span><p><b>ارسال امن به مشتری</b><small>پس از ثبت، لینک ورود امن از طریق پیامک در صف ارسال قرار میگیرد.</small></p></div><button type="submit" class="create-submit"><span>ثبت تیکت و ارسال لینک امن</span><small>این اقدام یک پرونده جدید ایجاد میکند</small></button><a href="{% url 'staff_tickets:list' %}" class="create-cancel">انصراف و بازگشت</a></div></aside>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<script defer src="{% static 'js/location-picker.js' %}?v={{ static_asset_version }}"></script><script defer src="{% static 'js/staff-customer-picker.js' %}?v={{ static_asset_version }}"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,9 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="staff-page-head customer-page-head"><div><p class="eyebrow">بانک مشتریان</p><h1>مشتریان</h1><p>مشتری را با نام یا موبایل پیدا کنید و وضعیت درخواستهای او را یکجا ببینید.</p></div>{% if perms.tickets.add_ticket %}<a class="button" href="{% url 'staff_tickets:create' %}">+ ثبت تیکت برای مشتری</a>{% endif %}</section>
|
||||
<section class="customer-directory card">
|
||||
<form class="customer-directory-search" hx-get="{% url 'staff_tickets:customers' %}" hx-target="#customer-results" hx-push-url="true" hx-trigger="keyup changed delay:400ms from:input[name=q], submit"><label>جستوجوی مشتری<input type="search" name="q" value="{{ search }}" placeholder="نام، نام خانوادگی یا شماره موبایل" autofocus></label><button type="submit">جستوجو</button>{% if search %}<a href="{% url 'staff_tickets:customers' %}">پاککردن</a>{% endif %}</form>
|
||||
<div id="customer-results">{% include 'tickets/_staff_customer_results.html' %}</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,37 @@
|
||||
{% 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:'' }}">
|
||||
<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>{% 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>
|
||||
</header>
|
||||
<div class="workspace-grid staff-conversation-grid">
|
||||
<aside class="workspace-side actions-side" id="ticket-actions" data-workspace-panel>
|
||||
<button type="button" class="panel-close" data-close-panel>×</button>
|
||||
<section class="info-card"><p class="panel-index">وضعیت تیکت</p><div class="info-list"><div class="info-row"><span>وضعیت</span><b>{{ ticket.get_status_display }}</b></div><div class="info-row"><span>اولویت</span><b>{{ ticket.get_priority_display }}</b></div><div class="info-row"><span>دسته</span><b>{{ ticket.category }}</b></div><div class="info-row"><span>مالک</span><b>{{ ticket.assigned_expert_name }}</b></div>{% if ticket.first_response_due_at %}<div class="info-row"><span>مهلت پاسخ</span><b>{{ ticket.first_response_due_at|jalali_datetime }}</b></div>{% endif %}</div></section>
|
||||
<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>
|
||||
{% 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>
|
||||
<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>
|
||||
<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 %}
|
||||
<div class="composer-context"><div><b x-text="mode==='note'?'ثبت یادداشت کارشناسی در پرونده':'ثبت پاسخ رسمی در تیکت'"></b><small x-text="mode==='note'?'این یادداشت فقط برای تیم کارشناسی قابل مشاهده است.':'پاسخ را کامل و یکجا بنویسید؛ این متن برای مشتری ارسال و اعلان میشود.'"></small></div><div class="composer-context-actions"><span data-draft-status></span><span class="composer-send-icon" role="button" tabindex="0" data-scroll-to-submit aria-label="رفتن به دکمه ثبت پاسخ" title="رفتن به دکمه ثبت پاسخ"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 20.5 21 12 3 3.5v6.6l12 1.9-12 1.9z"/></svg></span></div></div>
|
||||
{{ form.body }}{{ form.body.errors }}<div class="reply-media-picker"><div class="media-picker-actions"><label class="media-picker-button gallery-button" for="staff-reply-gallery">▣ انتخاب عکس از گالری</label><label class="media-picker-button" for="staff-reply-file">+ انتخاب فایل</label></div><input id="staff-reply-gallery" type="file" accept="image/*" multiple data-gallery-input><input id="staff-reply-file" 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><div class="file-preview" data-file-preview></div>
|
||||
<div class="composer-extra" hidden data-composer-extra>{{ form.mentioned_experts.label_tag }}{{ form.mentioned_experts }}</div>
|
||||
<div class="voice-recorder compact-recorder" data-voice-recorder hidden><div class="voice-head"><b>افزودن توضیح صوتی</b><span class="badge" data-timer>00:00</span></div><div class="voice-actions"><button type="button" data-record>● شروع ضبط</button><button type="button" data-pause disabled>مکث</button><button type="button" data-stop disabled>توقف</button><button type="button" class="ghost" data-delete hidden>حذف</button></div><audio controls hidden></audio></div>
|
||||
<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>
|
||||
</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' %}
|
||||
<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>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,16 @@
|
||||
{% extends 'base.html' %}
|
||||
{% 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>{% 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><label class="attachment-field"><span class="attachment-icon">+</span><div><b>{{ form.files.label }}</b><small>{{ form.files.help_text }}</small></div>{{ form.files }}{{ form.files.errors }}</label></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 %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}<div class="operations-page operations-dispatch-detail">
|
||||
<section class="staff-page-head">
|
||||
<div><p class="eyebrow">شناسه ارسال {{ dispatch.public_id }}</p><h1>{{ dispatch.subject }}</h1><p>{{ dispatch.unit }} · گروه {{ dispatch.contact_group.name }} · {{ dispatch.recipient_count }} مخاطب</p></div>
|
||||
<div><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 %}" style="margin-top:.75rem">{% csrf_token %}<button class="button" type="submit">تلاش مجدد برای {{ metrics.failed }} ارسال ناموفق</button></form>{% endif %}</div>
|
||||
</section>
|
||||
<section class="stats-grid operations-metrics" aria-label="وضعیت ارسال گروهی"><article class="stat-card"><div class="stat-label">پیامک موفق</div><div class="stat-value">{{ metrics.sent }}</div></article><article class="stat-card"><div class="stat-label">در صف</div><div class="stat-value">{{ metrics.pending }}</div></article><article class="stat-card"><div class="stat-label">لینک مشاهدهشده</div><div class="stat-value">{{ metrics.opened }}</div></article><article class="stat-card"><div class="stat-label">پاسخ ثبتشده</div><div class="stat-value">{{ metrics.replied }}</div></article><article class="stat-card alert"><div class="stat-label">ارسال ناموفق</div><div class="stat-value">{{ metrics.failed }}</div></article></section>
|
||||
<section class="ticket-stream">
|
||||
<header class="stream-head"><div><span class="panel-index">نمای مقایسهای</span><h2>وضعیت دریافت و پاسخ مخاطبان</h2></div><span>{{ dispatch.get_status_display }}</span></header>
|
||||
<div class="table-scroll"><table><thead><tr><th>مخاطب</th><th>شماره</th><th>تیکت</th><th>وضعیت پیامک</th><th>وضعیت تیکت</th><th>بازدید لینک</th><th>پاسخ پیشنهادی</th></tr></thead><tbody>
|
||||
{% for ticket in tickets %}
|
||||
<tr>
|
||||
<td>{{ ticket.customer.customer_profile|default:ticket.customer.username }}</td>
|
||||
<td dir="ltr">{{ ticket.customer.local_mobile }}</td>
|
||||
<td><a href="{% url 'staff_tickets:detail' ticket.public_id %}">{{ ticket.ticket_number }}</a></td>
|
||||
<td>{% if ticket.sms_job %}{% if ticket.sms_job.status == 'SENT' %}پذیرفتهشده توسط درگاه{% else %}{{ ticket.sms_job.get_status_display }}{% endif %}{% if ticket.sms_job.provider_message_id %}<small class="table-subtitle" dir="ltr">مرجع: {{ ticket.sms_job.provider_message_id|truncatechars:45 }}</small>{% endif %}{% if ticket.sms_job.last_error %}<small class="table-subtitle">{{ ticket.sms_job.last_error|truncatechars:60 }}</small>{% endif %}{% else %}ثبت نشده{% endif %}</td><td>{{ ticket.get_status_display }}</td>
|
||||
<td>{% for link in ticket.magic_links.all %}{{ link.view_count }}{% if not forloop.last %} + {% endif %}{% empty %}ارسال نشده{% endfor %}</td>
|
||||
<td>{% if ticket.latest_customer_response %}{{ ticket.latest_customer_response.body|truncatechars:120 }}<small class="table-subtitle">{{ ticket.latest_customer_response.created_at|date:'Y/m/d H:i' }}</small>{% else %}هنوز پاسخی ثبت نشده{% endif %}</td>
|
||||
</tr>
|
||||
{% empty %}<tr><td colspan="7">برای این ارسال تیکتی ثبت نشده است.</td></tr>{% endfor %}
|
||||
</tbody></table></div>
|
||||
</section></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends 'base.html' %}{% load static %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}<div class="operations-page operations-contact-form" data-location-picker data-locations-url="{% static 'data/iran-locations.json' %}">
|
||||
<section class="operations-hero"><div><p class="eyebrow">پرونده ارتباطی</p><h1>{{ form_title }}</h1><p>{{ form_help }}</p></div><div class="operations-hero-actions"><a class="button button-secondary" href="{% url 'staff_tickets:contacts' %}">بازگشت به مخاطبان</a></div></section>
|
||||
<form method="post" class="contact-editor" data-disable-on-submit>{% csrf_token %}
|
||||
{% if form.non_field_errors %}<div class="create-form-errors" role="alert">{{ form.non_field_errors }}</div>{% endif %}
|
||||
<section class="editor-section"><header><span>۱</span><div><h2>هویت و راه ارتباطی</h2><p>اطلاعات پایه مخاطب برای شناسایی و ارسال پیامک</p></div></header><div class="editor-grid"><label><span>{{ form.mobile.label }} <em>*</em></span>{{ form.mobile }}{{ form.mobile.errors }}<small>شماره معتبر به شکل 09xxxxxxxxx</small></label><label><span>{{ form.stakeholder_type.label }} <em>*</em></span>{{ form.stakeholder_type }}{{ form.stakeholder_type.errors }}</label><label><span>{{ form.first_name.label }} <em>*</em></span>{{ form.first_name }}{{ form.first_name.errors }}</label><label><span>{{ form.last_name.label }}</span>{{ form.last_name }}{{ form.last_name.errors }}</label><label class="editor-wide"><span>{{ form.organization_name.label }}</span>{{ form.organization_name }}{{ form.organization_name.errors }}</label></div></section>
|
||||
<section class="editor-section"><header><span>۲</span><div><h2>موقعیت جغرافیایی</h2><p>استان و شهر را از پایگاه مکانهای ایران انتخاب کنید</p></div></header><div class="editor-grid"><label><span>{{ form.province.label }}</span>{{ form.province }}{{ form.province.errors }}</label><label><span>{{ form.city.label }}</span>{{ form.city }}{{ form.city.errors }}</label></div><p class="location-status" data-location-hint>برای نمایش شهرها، ابتدا استان را انتخاب کنید.</p></section>
|
||||
<section class="editor-section"><header><span>۳</span><div><h2>گروهبندی و دسترسی</h2><p>حداقل یک گروه انتخاب کنید؛ عضویت همزمان در چند گروه پشتیبانی میشود</p></div></header><label class="group-selector"><span>{{ form.groups.label }} <em>*</em></span>{{ form.groups }}{{ form.groups.errors }}<small>{{ form.groups.help_text }}</small></label></section>
|
||||
<footer class="editor-actions"><button type="submit">ذخیره مخاطب</button><a href="{% url 'staff_tickets:contacts' %}">انصراف</a><span>اطلاعات ورود یا رمز عبور برای مخاطب ساخته نمیشود.</span></footer>
|
||||
</form></div>{% endblock %}
|
||||
{% block extra_scripts %}<script defer src="{% static 'js/location-picker.js' %}?v={{ static_asset_version }}"></script>{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}<div class="operations-page operations-groups">
|
||||
<section class="operations-hero"><div><p class="eyebrow">سازماندهی دفترچه</p><h1>گروههای مخاطبان</h1><p>مخاطبان را برای ارسالهای هدفمند و ایزوله، در گروههای متعلق به هر واحد سازماندهی کنید.</p></div><div class="operations-hero-actions"><a class="button button-secondary" href="{% url 'staff_tickets:contacts' %}">بازگشت به مخاطبان</a></div></section>
|
||||
<section class="operations-kpis compact"><article><span class="kpi-icon">#</span><div><b>{{ group_metrics.total }}</b><small>کل گروهها</small></div></article><article><span class="kpi-icon is-blue">✓</span><div><b>{{ group_metrics.active }}</b><small>گروه فعال</small></div></article><article><span class="kpi-icon is-violet">♙</span><div><b>{{ group_metrics.memberships }}</b><small>مجموع عضویتها</small></div></article></section>
|
||||
<div class="operations-split"><section class="operations-panel"><header class="panel-heading"><div><small>گروههای قابل دسترس</small><h2>فهرست گروهها</h2></div><span>{{ groups|length }} مورد</span></header><div class="group-card-list">{% for group in groups %}<article class="group-card"><span class="group-symbol">{{ group.name|slice:':1' }}</span><div><h3>{{ group.name }}</h3><p>{{ group.description|default:'بدون توضیح'|truncatechars:85 }}</p><small>{{ group.unit }} · {{ group.contact_count }} عضو</small></div><span class="status-pill {% if group.is_active %}is-success{% else %}is-muted{% endif %}">{% if group.is_active %}فعال{% else %}غیرفعال{% endif %}</span>{% if can_edit %}<a href="{% url 'staff_tickets:group_edit' group.pk %}">ویرایش</a>{% endif %}</article>{% empty %}<div class="empty-state">هنوز گروهی ثبت نشده است.</div>{% endfor %}</div></section>{% if can_edit %}<aside class="operations-form-side"><header><span>+</span><div><small>{% if form.instance.pk %}ویرایش اطلاعات{% else %}ساخت گروه{% endif %}</small><h2>{% if form.instance.pk %}{{ form.instance.name }}{% else %}گروه جدید{% endif %}</h2></div></header><form method="post" class="operations-form">{% csrf_token %}{% for field in form %}<label><span>{{ field.label }}</span>{{ field }}{{ field.errors }}</label>{% endfor %}<button type="submit">ذخیره گروه</button>{% if form.instance.pk %}<a href="{% url 'staff_tickets:groups' %}">انصراف از ویرایش</a>{% endif %}</form></aside>{% endif %}</div></div>{% endblock %}
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="staff-page-head">
|
||||
<div><p class="eyebrow">مرکز عملیات سازمانی</p><h1>{{ active_tab_title }}</h1><p>تیکتها را بر اساس اولویت بررسی کنید و هر درخواست را با پاسخ کامل به مرحله بعد ببرید.</p>{% if staff_operations_access.create_ticket %}<a class="button mt-3" href="{% url 'staff_tickets:create' %}">+ ثبت تیکت برای مخاطب</a>{% endif %}</div>
|
||||
<div class="staff-sync"><span class="live-dot"></span><div><b>فهرست زنده</b><small>بهروزرسانی هر ۴۵ ثانیه</small></div></div>
|
||||
</section>
|
||||
|
||||
<section class="stats-grid" aria-label="خلاصه صفها">
|
||||
<a class="stat-card {% if active_tab == 'new' %}is-active{% endif %}" href="?tab=new"><div class="stat-head"><span class="stat-icon">◌</span><span class="stat-label">صف جدید</span></div><div class="stat-value">{{ counts.new }}</div><small>در انتظار تخصیص</small></a>
|
||||
<a class="stat-card {% if active_tab == 'mine' %}is-active{% endif %}" href="?tab=mine"><div class="stat-head"><span class="stat-icon">◎</span><span class="stat-label">تیکتهای من</span></div><div class="stat-value">{{ counts.mine }}</div><small>پروندههای تحت مسئولیت</small></a>
|
||||
<a class="stat-card {% if active_tab == 'waiting_staff' %}is-active{% endif %}" href="?tab=waiting_staff"><div class="stat-head"><span class="stat-icon">↺</span><span class="stat-label">نیازمند پاسخ</span></div><div class="stat-value">{{ counts.waiting }}</div><small>در انتظار اقدام شما</small></a>
|
||||
<article class="stat-card alert"><div class="stat-head"><span class="stat-icon">!</span><span class="stat-label">عبور از SLA</span></div><div class="stat-value">{{ counts.overdue }}</div><small>نیازمند رسیدگی فوری</small></article>
|
||||
</section>
|
||||
|
||||
<nav class="queue-switcher" aria-label="صفهای تخصصی">
|
||||
<a class="{% if active_tab == '' %}active{% endif %}" href="{% url 'staff_tickets:list' %}">همه قابل مشاهده</a>
|
||||
<a class="{% if active_tab == 'selected' %}active{% endif %}" href="?tab=selected">انتخابی من</a>
|
||||
<a class="{% if active_tab == 'waiting_staff' %}active{% endif %}" href="?tab=waiting_staff">در انتظار من</a>
|
||||
<a class="{% if active_tab == 'waiting_customer' %}active{% endif %}" href="?tab=waiting_customer">در انتظار مشتری</a>
|
||||
<a class="{% if active_tab == 'resolved' %}active{% endif %}" href="?tab=resolved">حلشده</a>
|
||||
<a class="{% if active_tab == 'closed' %}active{% endif %}" href="?tab=closed">بسته</a>
|
||||
</nav>
|
||||
|
||||
<div class="dashboard">
|
||||
<aside class="filters">
|
||||
<div class="filter-heading"><div class="filter-title"><span class="filter-icon">⌕</span><div><span class="panel-index">فیلترهای ترکیبی</span><h3>جستوجو و فیلتر تیکتها</h3></div></div><a href="{% if active_tab %}?tab={{ active_tab }}{% else %}{% url 'staff_tickets:list' %}{% endif %}">پاککردن همه</a></div>
|
||||
<form class="filter-form" hx-get="{% url 'staff_tickets:list' %}" hx-target="#ticket-results" hx-push-url="true" hx-trigger="change delay:250ms, keyup changed delay:500ms from:input[name=q]">
|
||||
{% if active_tab %}<input type="hidden" name="tab" value="{{ active_tab }}">{% endif %}
|
||||
<label class="search-field">جستوجوی تیکت<input name="q" value="{{ request.GET.q }}" placeholder="شماره، موضوع، موبایل یا نام"></label>
|
||||
<label>وضعیت<select name="status"><option value="">همه وضعیتها</option>{% for value,label in statuses %}<option value="{{ value }}" {% if request.GET.status == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label>
|
||||
<label>اولویت<select name="priority"><option value="">همه اولویتها</option>{% for value,label in priorities %}<option value="{{ value }}" {% if request.GET.priority == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label>
|
||||
<label>دستهبندی<select name="category"><option value="">همه دستهها</option>{% for item in categories %}<option value="{{ item.pk }}" {% if request.GET.category == item.pk|stringformat:'s' %}selected{% endif %}>{{ item }}</option>{% endfor %}</select></label>
|
||||
<label>مالک<select name="owner"><option value="">همه کارشناسان</option>{% for profile in experts %}<option value="{{ profile.user_id }}" {% if request.GET.owner == profile.user_id|stringformat:'s' %}selected{% endif %}>{{ profile.display_name }}</option>{% endfor %}</select></label>
|
||||
<label>واحد ایجادکننده<select name="owning_unit"><option value="">همه واحدهای مجاز</option>{% for item in units %}<option value="{{ item.pk }}" {% if request.GET.owning_unit == item.pk|stringformat:'s' %}selected{% endif %}>{{ item }}</option>{% endfor %}</select></label>
|
||||
<label>واحد ارجاعشونده<select name="assigned_unit"><option value="">همه واحدهای مجاز</option>{% for item in units %}<option value="{{ item.pk }}" {% if request.GET.assigned_unit == item.pk|stringformat:'s' %}selected{% endif %}>{{ item }}</option>{% endfor %}</select></label>
|
||||
<label>نوع ارسال<select name="delivery_type"><option value="">فردی و گروهی</option>{% for value,label in delivery_types %}<option value="{{ value }}" {% if request.GET.delivery_type == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label>
|
||||
<label>نوع مخاطب<select name="stakeholder_type"><option value="">همه مخاطبان</option>{% for value,label in stakeholder_types %}<option value="{{ value }}" {% if request.GET.stakeholder_type == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label>
|
||||
<label>شخص دریافتکننده<input name="recipient" value="{{ request.GET.recipient }}" placeholder="نام، شرکت یا موبایل"></label>
|
||||
<label>گروه مخاطبان<select name="contact_group" multiple>{% for item in contact_groups %}<option value="{{ item.pk }}" {% if item.pk in selected_group_ids %}selected{% endif %}>{{ item }}</option>{% endfor %}</select></label>
|
||||
<label>برچسبها<select name="tag" multiple>{% for item in ticket_tags %}<option value="{{ item.pk }}" {% if item.pk in selected_tag_ids %}selected{% endif %}>{{ item }}</option>{% endfor %}</select></label>
|
||||
<label>از تاریخ<input type="date" name="created_from" value="{{ request.GET.created_from }}"></label>
|
||||
<label>تا تاریخ<input type="date" name="created_to" value="{{ request.GET.created_to }}"></label>
|
||||
<label>بروزرسانی از<input type="date" name="updated_from" value="{{ request.GET.updated_from }}"></label>
|
||||
<label>بروزرسانی تا<input type="date" name="updated_to" value="{{ request.GET.updated_to }}"></label>
|
||||
<label class="check"><input type="checkbox" name="unread" value="1" {% if request.GET.unread %}checked{% endif %}><span>فقط خواندهنشدهها</span></label>
|
||||
<button type="submit">اعمال فیلتر</button>
|
||||
</form>
|
||||
</aside>
|
||||
<section class="ticket-stream" aria-label="فهرست درخواستها">
|
||||
<header class="stream-head"><div><span class="panel-index">{{ active_tab_title }}</span><h2>تیکتهای قابل اقدام</h2></div><div class="stream-meta"><span>{{ tickets|length }} نتیجه</span><span class="stream-status"><i></i> زنده</span></div></header>
|
||||
<div id="ticket-results" hx-get="{% url 'staff_tickets:list' %}?{{ request.GET.urlencode }}" hx-trigger="every 45s" hx-swap="innerHTML">{% include 'tickets/_staff_ticket_results.html' %}</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}<div class="operations-page operations-audit">
|
||||
<section class="staff-page-head"><div><p class="eyebrow">امنیت و ردپای دسترسی</p><h1>ممیزی لینکهای جادویی</h1><p>{% if global_access %}نمای مدیریتی همه واحدها{% else %}نمای محدود به واحدهای مجاز شما{% endif %}؛ زمان، سازنده، گیرنده، IP و دستگاه هر بازدید ثبت میشود.</p></div></section>
|
||||
<section class="ticket-stream"><form method="get" class="operations-toolbar"><input name="q" value="{{ request.GET.q }}" placeholder="شماره تیکت، نام یا موبایل"><select name="unit"><option value="">همه واحدهای مجاز</option>{% for unit in units %}<option value="{{ unit.pk }}" {% if request.GET.unit == unit.pk|stringformat:'s' %}selected{% endif %}>{{ unit }}</option>{% endfor %}</select><select name="status"><option value="">همه وضعیتها</option><option value="active" {% if request.GET.status == 'active' %}selected{% endif %}>فعال</option><option value="opened" {% if request.GET.status == 'opened' %}selected{% endif %}>مشاهدهشده</option><option value="unopened" {% if request.GET.status == 'unopened' %}selected{% endif %}>بازنشده</option><option value="expired" {% if request.GET.status == 'expired' %}selected{% endif %}>منقضی</option><option value="revoked" {% if request.GET.status == 'revoked' %}selected{% endif %}>باطلشده</option></select><input type="date" name="created_from" value="{{ request.GET.created_from }}" aria-label="از تاریخ"><input type="date" name="created_to" value="{{ request.GET.created_to }}" aria-label="تا تاریخ"><button type="submit">اعمال فیلتر</button></form><div class="table-scroll"><table><thead><tr><th>ایجاد</th><th>تیکت / واحد</th><th>سازنده</th><th>گیرنده</th><th>اعتبار</th><th>بازدیدها</th><th>آخرین IP / دستگاه</th><th>اقدام</th></tr></thead><tbody>{% for link in links %}<tr><td>{{ link.created_at|date:'Y/m/d H:i:s' }}</td><td><a href="{% url 'staff_tickets:detail' link.ticket.public_id %}">{{ link.ticket.ticket_number }}</a><small class="table-subtitle">{{ link.created_unit|default:'بدون واحد' }}</small></td><td>{{ link.created_by|default:'سیستم' }}</td><td>{{ link.recipient_name|default:'—' }}<small class="table-subtitle" dir="ltr">{{ link.recipient_mobile }}</small></td><td>{% if link.revoked_at %}<span class="status-pill is-danger">لغوشده</span>{% elif link.expires_at < now %}<span class="status-pill is-muted">منقضی</span>{% else %}<span class="status-pill is-success">فعال</span>{% endif %}<small class="table-subtitle">تا {{ link.expires_at|date:'Y/m/d H:i' }}</small></td><td><b class="view-counter">{{ link.view_count }}</b>{% for hit in link.access_logs.all|slice:':3' %}<small class="table-subtitle">{{ hit.opened_at|date:'m/d H:i:s' }} · {{ hit.ip_address|default:'IP نامشخص' }}</small>{% endfor %}</td><td dir="ltr">{{ link.last_used_ip|default:'—' }}<small class="table-subtitle">{{ link.last_user_agent|truncatechars:55|default:'—' }}</small></td><td>{% if can_revoke and not link.revoked_at and link.expires_at > now %}<form method="post" action="{% url 'staff_tickets:magic_revoke' link.pk %}">{% csrf_token %}<button type="submit">ابطال فوری</button></form>{% else %}—{% endif %}</td></tr>{% empty %}<tr><td colspan="8" class="empty-state">لینکی مطابق جستوجو وجود ندارد.</td></tr>{% endfor %}</tbody></table></div>{% if page.paginator.num_pages > 1 %}<nav class="queue-switcher" aria-label="صفحهبندی">{% if page.has_previous %}<a href="?page={{ page.previous_page_number }}&q={{ request.GET.q|urlencode }}&unit={{ request.GET.unit }}&status={{ request.GET.status }}&created_from={{ request.GET.created_from }}&created_to={{ request.GET.created_to }}">صفحه قبل</a>{% endif %}<span>صفحه {{ page.number }} از {{ page.paginator.num_pages }}</span>{% if page.has_next %}<a href="?page={{ page.next_page_number }}&q={{ request.GET.q|urlencode }}&unit={{ request.GET.unit }}&status={{ request.GET.status }}&created_from={{ request.GET.created_from }}&created_to={{ request.GET.created_to }}">صفحه بعد</a>{% endif %}</nav>{% endif %}</section></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
{% block content %}<div class="operations-page operations-templates">
|
||||
<section class="operations-hero"><div><p class="eyebrow">پیامرسانی استاندارد</p><h1>الگوهای پیامک واحدها</h1><p>متنهای تاییدشده دیتابیس را برای هر رویداد مدیریت و با متغیرهای امن شخصیسازی کنید.</p></div><div class="template-guide"><span>✦</span><div><b>متغیرهای کنترلشده</b><small>از ورود تگ نامعتبر جلوگیری میشود</small></div></div></section>
|
||||
<div class="template-layout"><section class="operations-panel"><header class="panel-heading"><div><small>کتابخانه واحدهای شما و fallback سیستم</small><h2>الگوهای موجود در دیتابیس</h2></div><span>{{ templates|length }} الگو</span></header><div class="template-card-list">{% for item in templates %}<article class="template-card"><span class="template-icon">✉</span><div><h3>{{ item.title }}</h3><p>{{ item.get_event_type_display }}</p><small>{% if item.unit %}{{ item.unit }}{% else %}الگوی عمومی سیستم{% endif %} · بروزرسانی {{ item.updated_at|date:'Y/m/d' }}</small></div><span class="status-pill {% if item.is_active %}is-success{% else %}is-muted{% endif %}">{% if item.is_active %}فعال{% else %}غیرفعال{% endif %}</span>{% if item.unit %}<a href="{% url 'staff_tickets:sms_template_edit' item.pk %}">ویرایش</a>{% else %}<span class="status-pill is-muted">Fallback</span>{% endif %}</article>{% empty %}<div class="empty-state"><span>✉</span><h3>هیچ الگویی در دیتابیس وجود ندارد</h3><p>migration دادههای پیشفرض را ایجاد میکند؛ وضعیت migration را بررسی کنید.</p></div>{% endfor %}</div></section>
|
||||
<aside class="operations-form-side template-editor"><header><span>{% if form.instance.pk %}✎{% else %}+{% endif %}</span><div><small>{% if form.instance.pk %}ویرایش الگو{% else %}ساخت الگو{% endif %}</small><h2>{% if form.instance.pk %}{{ form.instance.title }}{% else %}الگوی اختصاصی جدید{% endif %}</h2></div></header><form method="post" class="operations-form">{% csrf_token %}{% if form.non_field_errors %}<div class="create-form-errors">{{ form.non_field_errors }}</div>{% endif %}{% for field in form %}<label><span>{{ field.label }}</span>{{ field }}{{ field.errors }}</label>{% endfor %}<div class="variable-box"><b>متغیرهای قابل استفاده</b><div dir="ltr">{% for tag in allowed_tags %}<code>{% templatetag openvariable %}{{ tag }}{% templatetag closevariable %}</code>{% endfor %}</div></div><button type="submit">ذخیره الگوی پیامک</button>{% if form.instance.pk %}<a href="{% url 'staff_tickets:sms_templates' %}">انصراف از ویرایش</a>{% endif %}</form></aside></div>
|
||||
</div>{% endblock %}
|
||||
Reference in new issue
Block a user