fix del contact from group and image for ticket
This commit is contained in:
1 parent
5fc9a908c5
commit
a18a68578f
7 files changed
+168
-9
No files matched your search
@@ -181,7 +181,13 @@ class StaffContactForm(forms.Form):
|
||||
last_name = forms.CharField(label="نام خانوادگی", max_length=100, required=False)
|
||||
organization_name = forms.CharField(label="نام شرکت / سازمان", max_length=180, required=False)
|
||||
stakeholder_type = forms.ChoiceField(label="نوع مخاطب", choices=CustomerProfile.StakeholderType.choices)
|
||||
groups = forms.ModelMultipleChoiceField(label="عضویت در گروهها", queryset=ContactGroup.objects.none(), required=True, help_text="حداقل یک گروه برای تعیین مالکیت واحدی مخاطب انتخاب کنید.")
|
||||
groups = forms.ModelMultipleChoiceField(
|
||||
label="عضویت در گروهها",
|
||||
queryset=ContactGroup.objects.none(),
|
||||
required=True,
|
||||
help_text="گروههای موردنظر را روشن یا خاموش کنید؛ حداقل یک گروه باید باقی بماند.",
|
||||
widget=forms.CheckboxSelectMultiple(attrs={"data-contact-group-option": ""}),
|
||||
)
|
||||
province = forms.ChoiceField(label="استان", required=False)
|
||||
city = forms.ChoiceField(label="شهر", required=False)
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
.contact-card-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.8rem;padding:1rem;background:#f7fafc}.contact-card{display:grid;gap:.75rem;min-width:0;border:1px solid #dce7ef;border-radius:1rem;background:#fff;padding:1rem;transition:.18s ease;box-shadow:0 10px 28px -26px #08376b}.contact-card:hover{transform:translateY(-2px);border-color:#9dcbe2;box-shadow:0 18px 36px -29px #08376b}.contact-card header{display:flex;align-items:center;gap:.7rem}.contact-avatar{display:grid;width:2.65rem;height:2.65rem;flex:none;place-items:center;border-radius:.8rem;background:linear-gradient(145deg,#dff4ff,#dff7ef);color:#084d73;font-size:1rem;font-weight:900}.contact-identity{min-width:0;flex:1}.contact-identity h2{overflow:hidden;margin:0;color:#153f60;font-size:.88rem;text-overflow:ellipsis;white-space:nowrap}.contact-identity p{overflow:hidden;margin:.15rem 0 0;color:#718698;font-size:.63rem;text-overflow:ellipsis;white-space:nowrap}.contact-type{flex:none;border-radius:999px;background:#e7f4fb;padding:.2rem .48rem;color:#146b98;font-size:.57rem;font-weight:900}.contact-type.type-supplier{background:#fff3d8;color:#966000}.contact-type.type-carrier{background:#f0eaff;color:#6841b8}.contact-meta{display:flex;flex-wrap:wrap;gap:.4rem .9rem;color:#5f7689;font-size:.64rem}.contact-groups{display:flex;min-height:1.6rem;flex-wrap:wrap;gap:.3rem}.contact-groups span{border-radius:999px;background:#edf5f8;padding:.18rem .48rem;color:#49677c;font-size:.58rem;font-weight:800}.contact-groups small{color:#93a2ae;font-size:.6rem}.contact-ticket-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:.35rem}.contact-ticket-stats span{display:grid;gap:.1rem;border-radius:.6rem;background:#f3f6f8;padding:.45rem;text-align:center;color:#718394;font-size:.55rem}.contact-ticket-stats b{color:#173f60;font-size:.82rem}.contact-ticket-stats .needs-action{background:#fff5df;color:#916000}.contact-ticket-stats .needs-action b{color:#c37700}.contact-card footer{display:flex;align-items:center;justify-content:space-between;border-top:1px solid #edf2f5;padding-top:.7rem}.contact-card footer a{color:#557187;font-size:.64rem;font-weight:900}.contact-card footer .primary-action{border-radius:.6rem;background:#e8f6ff;padding:.4rem .65rem;color:#0875b5}.contact-empty{grid-column:1/-1}.contact-empty>span{display:grid;width:3rem;height:3rem;margin:0 auto .5rem;place-items:center;border-radius:1rem;background:#eaf5fb;color:#1679aa;font-size:1.3rem}
|
||||
.operations-split,.template-layout,.dispatch-layout{display:grid;grid-template-columns:minmax(0,1fr) 20rem;align-items:start;gap:1rem}.panel-heading{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.1rem;border-bottom:1px solid #e5edf3;background:#f8fbfd}.panel-heading small{color:#74889a;font-size:.6rem}.panel-heading h2{margin:.15rem 0 0;color:#173f60;font-size:.95rem}.panel-heading>span{border-radius:999px;background:#e7f4fb;padding:.3rem .6rem;color:#126c9d;font-size:.6rem;font-weight:900}.group-card-list,.template-card-list{display:grid;gap:.55rem;padding:.8rem}.group-card,.template-card{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;align-items:center;gap:.75rem;border:1px solid #e0e9ef;border-radius:.8rem;padding:.75rem;transition:.15s}.group-card:hover,.template-card:hover{border-color:#acd2e5;background:#fafdff}.group-symbol,.template-icon{display:grid;width:2.35rem;height:2.35rem;place-items:center;border-radius:.7rem;background:#e8f5fb;color:#0870a6;font-weight:900}.group-card h3,.template-card h3{margin:0;color:#173f60;font-size:.78rem}.group-card p,.template-card p{margin:.12rem 0;color:#667d90;font-size:.62rem}.group-card small,.template-card small{color:#8a9aa7;font-size:.56rem}.group-card>a,.template-card>a{color:#0875b5;font-size:.62rem;font-weight:900}.operations-form-side{overflow:hidden;border:1px solid #d8e4ec;border-radius:1rem;background:#fff;box-shadow:0 18px 40px -36px #08376b;position:sticky;top:5.6rem}.operations-form-side>header{display:flex;align-items:center;gap:.65rem;padding:1rem;background:linear-gradient(135deg,#0a4d78,#087b91);color:#fff}.operations-form-side>header>span{display:grid;width:2rem;height:2rem;place-items:center;border-radius:.6rem;background:rgba(255,255,255,.16);font-weight:900}.operations-form-side header small{font-size:.56rem;color:rgba(255,255,255,.65)}.operations-form-side header h2{margin:.12rem 0 0;font-size:.85rem}.operations-form{display:grid;gap:.75rem;padding:1rem}.operations-form label{display:grid;gap:.35rem}.operations-form label>span{color:#405e75;font-size:.65rem;font-weight:900}.operations-form input,.operations-form select,.operations-form textarea{width:100%;min-height:2.65rem;border:1px solid #c8d8e4;border-radius:.7rem;background:#fbfdff;padding:.6rem .7rem;color:#143e60}.operations-form textarea{min-height:6rem;resize:vertical}.operations-form button{min-height:2.75rem;border:0;border-radius:.7rem;background:linear-gradient(135deg,#0879b7,#075b8d);color:#fff;font-size:.7rem;font-weight:900}.operations-form>a{text-align:center;color:#6e8293;font-size:.62rem;font-weight:800}.operations-form .errorlist,.editor-section .errorlist,.dispatch-composer .errorlist{margin:0;padding:0;color:#b42318;font-size:.6rem;list-style:none}
|
||||
.contact-editor,.dispatch-composer{display:grid;gap:.85rem}.contact-editor{max-width:58rem;width:100%;margin-inline:auto}.editor-section,.dispatch-composer>section{overflow:hidden;border:1px solid #dbe6ee;border-radius:1rem;background:#fff;box-shadow:0 15px 38px -35px #08376b}.editor-section>header,.dispatch-composer>section>header{display:flex;align-items:center;gap:.7rem;border-bottom:1px solid #e8eef3;padding:.85rem 1rem;background:#f9fcfe}.editor-section>header>span,.dispatch-composer>section>header>span{display:grid;width:2rem;height:2rem;flex:none;place-items:center;border-radius:.65rem;background:linear-gradient(135deg,#0a699f,#098d9c);color:#fff;font-size:.7rem;font-weight:900}.editor-section h2,.dispatch-composer h2{margin:0;color:#173f60;font-size:.82rem}.editor-section header p,.dispatch-composer header p{margin:.15rem 0 0;color:#73889a;font-size:.6rem}.editor-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.8rem;padding:1rem}.editor-grid label,.group-selector,.message-editor{display:grid;align-content:start;gap:.35rem;color:#3f5f77;font-size:.65rem;font-weight:900}.editor-grid input,.editor-grid select,.group-selector select,.message-editor textarea{width:100%;min-height:2.75rem;border:1px solid #c8d8e4;border-radius:.72rem;background:#fbfdff;padding:.62rem .72rem;color:#123f61}.editor-grid input:focus,.editor-grid select:focus,.group-selector select:focus,.message-editor textarea:focus{border-color:#1689bd;outline:3px solid rgba(22,137,189,.11);background:#fff}.editor-grid small,.group-selector small{color:#7a8e9e;font-size:.56rem;font-weight:500}.editor-grid em,.group-selector em{color:#d05c3b;font-style:normal}.editor-wide{grid-column:1/-1}.group-selector,.message-editor{margin:1rem}.group-selector select{min-height:9rem}.location-status{margin:-.35rem 1rem .85rem;border-radius:.6rem;background:#edf7fc;padding:.55rem .7rem;color:#39708e;font-size:.58rem}.location-status.is-error{background:#fff0ed;color:#b04432}.editor-actions{display:flex;align-items:center;gap:.7rem;border:1px solid #d9e5ed;border-radius:1rem;background:#fff;padding:.8rem}.editor-actions button{min-height:2.75rem;border:0;border-radius:.7rem;background:linear-gradient(135deg,#0879b7,#075b8d);padding:.65rem 1.4rem;color:#fff;font-weight:900}.editor-actions a{color:#657d90;font-size:.65rem;font-weight:900}.editor-actions span{margin-right:auto;color:#8798a5;font-size:.58rem}
|
||||
.group-selector{border:0}.group-selector legend{color:#3f5f77;font-size:.7rem;font-weight:900}.group-selector-tools{display:grid;grid-template-columns:minmax(12rem,1fr) auto auto;align-items:center;gap:.55rem;margin:.4rem 0 .65rem}.group-selector-tools label{display:block}.group-selector-tools input{width:100%;min-height:2.65rem;border:1px solid #c8d8e4;border-radius:.72rem;background:#fbfdff;padding:.58rem .75rem}.group-selection-count{border-radius:999px;background:#e7f4fb;padding:.42rem .7rem;color:#126c9d;white-space:nowrap}.group-selector-tools button{min-height:2.55rem;border:1px solid #d8e4ec;border-radius:.65rem;background:#fff;padding:.45rem .7rem;color:#64798c;font-size:.6rem;font-weight:900}.group-selector-tools button:disabled{cursor:not-allowed;opacity:.45}.group-choice-list>div{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.55rem}.group-choice-list>div>div{border:1px solid #dbe5ec;border-radius:.75rem;background:#fbfdff;transition:.15s}.group-choice-list label{display:flex;align-items:center;gap:.65rem;min-height:3.15rem;padding:.65rem .75rem;cursor:pointer;color:#35566e;font-size:.65rem}.group-choice-list input{width:1.1rem;height:1.1rem;flex:none;accent-color:#0879a7}.group-choice-list>div>div.is-selected{border-color:#58aeca;background:#eaf8fb;box-shadow:0 0 0 2px rgba(22,137,189,.08)}.group-choice-list>div>div[hidden]{display:none}.group-empty{margin:.5rem 0;border-radius:.65rem;background:#f5f8fa;padding:.75rem;color:#758999;font-size:.62rem}.staff-attachment-picker{display:grid;gap:.8rem}.attachment-picker-copy{display:flex;align-items:center;gap:.7rem}.staff-attachment-picker .media-picker-actions{margin:0}.staff-attachment-picker .file-preview:empty{display:none}
|
||||
.dispatch-composer>footer{display:flex;align-items:center;justify-content:space-between;gap:1rem;border:1px solid #d7e4ec;border-radius:1rem;background:#fff;padding:.9rem 1rem}.dispatch-composer footer b,.dispatch-composer footer small{display:block}.dispatch-composer footer b{color:#173f60;font-size:.68rem}.dispatch-composer footer small{margin-top:.18rem;color:#73889a;font-size:.56rem}.dispatch-composer footer button{min-height:2.8rem;border:0;border-radius:.72rem;background:linear-gradient(135deg,#0879b7,#087c86);padding:.65rem 1rem;color:#fff;font-size:.68rem;font-weight:900}.message-editor textarea{min-height:11rem;resize:vertical;line-height:1.9}.dispatch-history{overflow:hidden;border:1px solid #d8e4ec;border-radius:1rem;background:#fff;position:sticky;top:5.6rem}.dispatch-history>header{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #e5edf3;padding:1rem}.dispatch-history header small{color:#7b8d9d;font-size:.56rem}.dispatch-history header h2{margin:.12rem 0 0;color:#173f60;font-size:.88rem}.dispatch-history header>span{display:grid;width:2rem;height:2rem;place-items:center;border-radius:.65rem;background:#e8f4fb;color:#0873a9;font-size:.65rem;font-weight:900}.dispatch-history>div{display:grid;max-height:39rem;overflow:auto}.dispatch-history-card{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:.55rem;border-bottom:1px solid #edf2f5;padding:.75rem;color:inherit}.dispatch-history-card:hover{background:#f7fbfd}.dispatch-state{width:.5rem;height:.5rem;border-radius:50%;background:#8da0ae}.dispatch-state.state-sent{background:#12a36d}.dispatch-state.state-queued{background:#e19b18}.dispatch-state.state-partial{background:#dc5c42}.dispatch-history-card b{display:block;overflow:hidden;color:#24485f;font-size:.68rem;text-overflow:ellipsis;white-space:nowrap}.dispatch-history-card small{display:block;margin-top:.15rem;color:#718698;font-size:.56rem}.dispatch-history-card p{display:flex;gap:.6rem;margin:.35rem 0 0;color:#8798a5;font-size:.52rem}.dispatch-history-card em{color:#8aa0b0;font-style:normal}
|
||||
.template-card-list{max-height:44rem;overflow:auto}.variable-box{border:1px dashed #b9cedb;border-radius:.7rem;background:#f7fbfd;padding:.7rem}.variable-box>b{display:block;margin-bottom:.45rem;color:#3e6077;font-size:.62rem}.variable-box>div{display:flex;flex-wrap:wrap;gap:.3rem}.variable-box code{border-radius:.35rem;background:#e7f3f9;padding:.18rem .35rem;color:#075e8d;font-size:.53rem}
|
||||
@media(max-width:1100px){.operations-kpis{grid-template-columns:repeat(3,1fr)}.contact-filter-bar{grid-template-columns:repeat(2,minmax(0,1fr))}.contact-search{grid-column:1/-1}.operations-split,.template-layout,.dispatch-layout{grid-template-columns:1fr}.operations-form-side,.dispatch-history{position:static}.dispatch-history>div,.template-card-list{max-height:none}}
|
||||
@media(max-width:700px){.operations-hero{align-items:stretch;flex-direction:column;padding:1.1rem}.operations-hero h1{font-size:1.3rem}.operations-hero-actions{align-items:stretch;flex-direction:column}.security-callout,.template-guide{align-self:stretch}.operations-kpis,.operations-kpis.compact{display:flex;max-width:none;overflow-x:auto;padding-bottom:.25rem}.operations-kpis article{min-width:9rem}.contact-filter-bar{grid-template-columns:1fr}.contact-search{grid-column:auto}.contact-card-grid{grid-template-columns:1fr;padding:.65rem}.contact-result-meta{align-items:flex-start;flex-direction:column;gap:.4rem}.editor-grid{grid-template-columns:1fr;padding:.8rem}.editor-wide{grid-column:auto}.editor-actions,.dispatch-composer>footer{align-items:stretch;flex-direction:column}.editor-actions span{margin:0}.group-card,.template-card{grid-template-columns:auto minmax(0,1fr) auto}.group-card .status-pill,.template-card .status-pill{grid-column:2}.group-card>a,.template-card>a{grid-column:3;grid-row:1/3}.contact-ticket-stats{grid-template-columns:repeat(2,1fr)}}
|
||||
@media(max-width:700px){.operations-hero{align-items:stretch;flex-direction:column;padding:1.1rem}.operations-hero h1{font-size:1.3rem}.operations-hero-actions{align-items:stretch;flex-direction:column}.security-callout,.template-guide{align-self:stretch}.operations-kpis,.operations-kpis.compact{display:flex;max-width:none;overflow-x:auto;padding-bottom:.25rem}.operations-kpis article{min-width:9rem}.contact-filter-bar{grid-template-columns:1fr}.contact-search{grid-column:auto}.contact-card-grid{grid-template-columns:1fr;padding:.65rem}.contact-result-meta{align-items:flex-start;flex-direction:column;gap:.4rem}.editor-grid{grid-template-columns:1fr;padding:.8rem}.editor-wide{grid-column:auto}.editor-actions,.dispatch-composer>footer{align-items:stretch;flex-direction:column}.editor-actions span{margin:0}.group-selector-tools{grid-template-columns:1fr}.group-selection-count{justify-self:start}.group-choice-list>div{grid-template-columns:1fr}.group-card,.template-card{grid-template-columns:auto minmax(0,1fr) auto}.group-card .status-pill,.template-card .status-pill{grid-column:2}.group-card>a,.template-card>a{grid-column:3;grid-row:1/3}.contact-ticket-stats{grid-template-columns:repeat(2,1fr)}}
|
||||
@@ -0,0 +1,35 @@
|
||||
document.querySelectorAll('[data-contact-group-selector]').forEach(selector => {
|
||||
const search = selector.querySelector('[data-group-search]');
|
||||
const count = selector.querySelector('[data-group-selection-count]');
|
||||
const clear = selector.querySelector('[data-clear-groups]');
|
||||
const empty = selector.querySelector('[data-group-empty]');
|
||||
const choices = Array.from(selector.querySelectorAll('.group-choice-list > div > div'));
|
||||
|
||||
const update = () => {
|
||||
let selected = 0;
|
||||
let visible = 0;
|
||||
const query = (search?.value || '').trim().toLocaleLowerCase('fa');
|
||||
choices.forEach(choice => {
|
||||
const input = choice.querySelector('input[type="checkbox"]');
|
||||
const matches = !query || choice.textContent.toLocaleLowerCase('fa').includes(query);
|
||||
choice.hidden = !matches;
|
||||
choice.classList.toggle('is-selected', Boolean(input?.checked));
|
||||
selected += input?.checked ? 1 : 0;
|
||||
visible += matches ? 1 : 0;
|
||||
});
|
||||
if (count) count.textContent = `${selected.toLocaleString('fa-IR')} گروه انتخاب شده`;
|
||||
if (clear) clear.disabled = selected === 0;
|
||||
if (empty) empty.hidden = visible !== 0;
|
||||
};
|
||||
|
||||
selector.addEventListener('change', update);
|
||||
search?.addEventListener('input', update);
|
||||
clear?.addEventListener('click', () => {
|
||||
choices.forEach(choice => {
|
||||
const input = choice.querySelector('input[type="checkbox"]');
|
||||
if (input) input.checked = false;
|
||||
});
|
||||
update();
|
||||
});
|
||||
update();
|
||||
});
|
||||
@@ -1,11 +1,13 @@
|
||||
{% 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="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>
|
||||
<header class="create-page-head"><div><a class="create-back" href="{% url 'staff_tickets:list' %}">→ بازگشت به میز کار</a><p class="eyebrow">ایجاد پرونده از طرف {{ staff_unit.name|default:'واحد سازمانی' }}</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 }}
|
||||
<form method="post" enctype="multipart/form-data" data-disable-on-submit data-ticket-composer 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>
|
||||
@@ -14,11 +16,11 @@
|
||||
<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>
|
||||
<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><div class="upload-zone media-picker staff-attachment-picker"><div class="attachment-picker-copy"><span class="attachment-icon">+</span><div><b>{{ form.files.label }}</b><small>چند عکس، فایل یا سند را انتخاب کنید؛ پیش از ثبت میتوانید هر مورد را جداگانه حذف کنید.</small></div></div><div class="media-picker-actions"><label class="media-picker-button gallery-button" for="staff-ticket-gallery-input">▣ انتخاب عکس از گالری</label><label class="media-picker-button" for="staff-ticket-file-input">+ انتخاب فایل</label></div><input id="staff-ticket-gallery-input" type="file" accept="image/*" multiple data-gallery-input><input id="staff-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 aria-live="polite"></div></div></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>
|
||||
<script defer src="{% static 'vendor/compressor.min.js' %}?v={{ static_asset_version }}"></script><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><script defer src="{% static 'js/attachment-uploader.js' %}?v={{ static_asset_version }}"></script>
|
||||
{% endblock %}
|
||||
@@ -6,7 +6,7 @@
|
||||
{% 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>
|
||||
<section class="editor-section"><header><span>۳</span><div><h2>گروهبندی و دسترسی</h2><p>با یک کلیک عضویت مخاطب را در گروههای قابل مدیریت اضافه یا حذف کنید</p></div></header><fieldset class="group-selector" data-contact-group-selector><legend>{{ form.groups.label }} <em>*</em></legend><div class="group-selector-tools"><label><span class="sr-only">جستوجوی گروه</span><input type="search" placeholder="جستوجوی نام گروه یا واحد…" autocomplete="off" data-group-search></label><span class="group-selection-count" data-group-selection-count aria-live="polite">۰ گروه انتخاب شده</span><button type="button" data-clear-groups>پاککردن انتخابها</button></div><div class="group-choice-list">{{ form.groups }}</div>{{ form.groups.errors }}<small>{{ form.groups.help_text }}</small><p class="group-empty" data-group-empty hidden>گروهی با این عبارت پیدا نشد.</p></fieldset></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 %}
|
||||
{% block extra_scripts %}<script defer src="{% static 'js/location-picker.js' %}?v={{ static_asset_version }}"></script><script defer src="{% static 'js/contact-group-selector.js' %}?v={{ static_asset_version }}"></script>{% endblock %}
|
||||
@@ -1,3 +1,5 @@
|
||||
import base64
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.urls import reverse
|
||||
@@ -30,7 +32,7 @@ def test_staff_operations_pages_are_authorized_and_visually_styled(live_server,
|
||||
unit = OrganizationalUnit.objects.create(name="عملیات E2E", slug="operations-e2e")
|
||||
permissions = {
|
||||
"customers": {"view_customerprofile", "add_customerprofile", "change_customerprofile", "view_contactgroup", "add_contactgroup", "change_contactgroup"},
|
||||
"tickets": {"view_ticket", "dispatch_group_tickets"},
|
||||
"tickets": {"view_ticket", "add_ticket", "dispatch_group_tickets"},
|
||||
"notifications": {"view_smstemplate", "add_smstemplate", "change_smstemplate"},
|
||||
"accounts": {"view_ticketmagiclink"},
|
||||
}
|
||||
@@ -45,6 +47,7 @@ def test_staff_operations_pages_are_authorized_and_visually_styled(live_server,
|
||||
customer = User.objects.create_user(username="e2e-carrier", mobile="09120009121", account_type=User.AccountType.CUSTOMER)
|
||||
profile = CustomerProfile.objects.create(user=customer, first_name="باربری", stakeholder_type=CustomerProfile.StakeholderType.CARRIER)
|
||||
group = ContactGroup.objects.create(unit=unit, name="باربریها")
|
||||
secondary_group = ContactGroup.objects.create(unit=unit, name="مخاطبان منتخب")
|
||||
profile.groups.add(group)
|
||||
category = TicketCategory.objects.create(title="استعلام E2E", slug="operations-e2e")
|
||||
ticket = create_ticket(customer=customer, created_by=staff, unit=unit, category=category, subject="استعلام آزمایشی", body="قیمت را اعلام کنید")
|
||||
@@ -86,3 +89,29 @@ def test_staff_operations_pages_are_authorized_and_visually_styled(live_server,
|
||||
expect(province.locator("option")).not_to_have_count(1)
|
||||
province.select_option(label="اصفهان")
|
||||
expect(city.locator("option", has_text="کاشان")).to_have_count(1)
|
||||
|
||||
page.goto(f"{live_server.url}{reverse('staff_tickets:contact_edit', args=[customer.public_id])}", wait_until="domcontentloaded")
|
||||
first_group = page.locator(f'input[name="groups"][value="{group.pk}"]')
|
||||
second_group = page.locator(f'input[name="groups"][value="{secondary_group.pk}"]')
|
||||
expect(first_group).to_be_checked()
|
||||
expect(second_group).not_to_be_checked()
|
||||
second_group.check()
|
||||
first_group.uncheck()
|
||||
expect(page.locator("[data-group-selection-count]")).to_contain_text("۱")
|
||||
page.get_by_role("button", name="ذخیره مخاطب").click()
|
||||
page.wait_for_url(f"**{reverse('staff_tickets:contacts')}")
|
||||
page.goto(f"{live_server.url}{reverse('staff_tickets:contact_edit', args=[customer.public_id])}", wait_until="domcontentloaded")
|
||||
expect(page.locator(f'input[name="groups"][value="{group.pk}"]')).not_to_be_checked()
|
||||
expect(page.locator(f'input[name="groups"][value="{secondary_group.pk}"]')).to_be_checked()
|
||||
|
||||
page.goto(f"{live_server.url}{reverse('staff_tickets:create')}", wait_until="domcontentloaded")
|
||||
tiny_png = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=")
|
||||
page.locator("#staff-ticket-gallery-input").set_input_files([
|
||||
{"name": "first.png", "mimeType": "image/png", "buffer": tiny_png},
|
||||
{"name": "second.png", "mimeType": "image/png", "buffer": tiny_png},
|
||||
])
|
||||
expect(page.locator("[data-file-preview] .file-chip")).to_have_count(2)
|
||||
expect(page.locator("[data-file-preview] .file-chip img")).to_have_count(2)
|
||||
page.locator("[data-file-preview] .file-chip button").first.click()
|
||||
expect(page.locator("[data-file-preview] .file-chip")).to_have_count(1)
|
||||
assert page.locator('input[name="files"]').evaluate("input => input.files.length") == 1
|
||||
@@ -257,6 +257,92 @@ def test_superuser_without_staff_profile_can_open_all_staff_contacts(client):
|
||||
assert "customer-without-profile" not in response.content.decode()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_staff_can_visually_add_and_remove_contact_group_memberships(client):
|
||||
unit = OrganizationalUnit.objects.create(name="واحد ویرایش مخاطب", slug="contact-membership-editor")
|
||||
unit.permissions.add(*Permission.objects.filter(
|
||||
content_type__app_label="customers",
|
||||
codename__in=("view_customerprofile", "change_customerprofile"),
|
||||
))
|
||||
staff = make_staff("contact-membership-agent", unit)
|
||||
contact = make_contact("editable-contact", "09120000505")
|
||||
first = ContactGroup.objects.create(name="گروه اولیه", unit=unit)
|
||||
second = ContactGroup.objects.create(name="گروه جایگزین", unit=unit)
|
||||
contact.customer_profile.groups.add(first)
|
||||
client.force_login(staff)
|
||||
url = reverse("staff_tickets:contact_edit", args=[contact.public_id])
|
||||
|
||||
page = client.get(url)
|
||||
content = page.content.decode()
|
||||
assert page.status_code == 200
|
||||
assert "data-contact-group-selector" in content
|
||||
assert "data-group-search" in content
|
||||
assert str(first.pk) in {str(value) for value in page.context["form"]["groups"].value()}
|
||||
assert f'value="{second.pk}"' in content
|
||||
|
||||
response = client.post(url, {
|
||||
"mobile": contact.local_mobile,
|
||||
"first_name": "مخاطب",
|
||||
"last_name": "ویرایششده",
|
||||
"organization_name": "",
|
||||
"stakeholder_type": CustomerProfile.StakeholderType.CUSTOMER,
|
||||
"groups": [second.pk],
|
||||
"province": "",
|
||||
"city": "",
|
||||
})
|
||||
|
||||
assert response.status_code == 302
|
||||
assert set(contact.customer_profile.groups.values_list("pk", flat=True)) == {second.pk}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_staff_ticket_create_supports_multiple_previewable_attachments(client, monkeypatch, settings, tmp_path):
|
||||
settings.MEDIA_ROOT = tmp_path
|
||||
unit = OrganizationalUnit.objects.create(name="واحد پیوست کارشناس", slug="staff-attachment-unit")
|
||||
unit.permissions.add(Permission.objects.get(content_type__app_label="tickets", codename="add_ticket"))
|
||||
staff = make_staff("staff-attachment-agent", unit)
|
||||
category = TicketCategory.objects.create(unit=unit, title="مدارک", slug="staff-attachment-category")
|
||||
AttachmentPolicy.objects.create(attachment_type="IMAGE", max_file_size=1024 * 1024, allowed_extensions=[".jpg"])
|
||||
AttachmentPolicy.objects.create(attachment_type="PDF", max_file_size=1024 * 1024, allowed_extensions=[".pdf"])
|
||||
monkeypatch.setattr(
|
||||
"apps.attachments.services.magic.from_buffer",
|
||||
lambda content, mime=True: "image/jpeg" if content.startswith(b"\xff\xd8") else "application/pdf",
|
||||
)
|
||||
client.force_login(staff)
|
||||
url = reverse("staff_tickets:create")
|
||||
|
||||
page = client.get(url)
|
||||
content = page.content.decode()
|
||||
assert page.status_code == 200
|
||||
assert "data-ticket-composer" in content
|
||||
assert "data-gallery-input" in content
|
||||
assert "data-document-input" in content
|
||||
assert "data-file-preview" in content
|
||||
assert "attachment-uploader.js" in content
|
||||
assert "ایجاد پرونده از طرف واحد پیوست کارشناس" in content
|
||||
|
||||
response = client.post(url, {
|
||||
"unit": unit.pk,
|
||||
"customer_mobile": "09120000506",
|
||||
"first_name": "مشتری",
|
||||
"last_name": "پیوست",
|
||||
"stakeholder_type": CustomerProfile.StakeholderType.CUSTOMER,
|
||||
"category": category.pk,
|
||||
"subject": "ارسال همزمان تصویر و سند",
|
||||
"body": "دو فایل برای این پرونده ثبت میشود.",
|
||||
"files": [
|
||||
SimpleUploadedFile("photo.jpg", b"\xff\xd8\xff\xe0image", content_type="image/jpeg"),
|
||||
SimpleUploadedFile("document.pdf", b"%PDF-1.4\n%%EOF", content_type="application/pdf"),
|
||||
],
|
||||
})
|
||||
|
||||
assert response.status_code == 302
|
||||
ticket = Ticket.objects.get(subject="ارسال همزمان تصویر و سند")
|
||||
assert set(ticket.ticket_messages.get().attachments.values_list("original_name", flat=True)) == {
|
||||
"photo.jpg", "document.pdf",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_unit_scoped_add_ticket_preserves_contact_ownership_and_stakeholder(client):
|
||||
unit=OrganizationalUnit.objects.create(name="حسابداری عملیاتی",slug="accounting-ops")
|
||||
|
||||
Reference in new issue
Block a user