fix pwa, wizard focus, supervisor 5

This commit is contained in:
javad committed 2026-09-16 11:06:38 +03:30
1 parent ffc1ec0dcf
commit d72381f0ac
11 files changed
+99 -25

No files matched your search

+2 -2
View File
@@ -15,13 +15,13 @@ class UnitMembershipInline(admin.TabularInline):
class OrganizationalUnitAdmin(admin.ModelAdmin):
list_display = ("name", "slug", "bale_chat_id", "sort_order", "show_in_customer_form", "accepts_external_tickets", "is_active", "updated_at")
list_filter = ("show_in_customer_form", "accepts_external_tickets", "is_active")
search_fields = ("name", "slug", "bale_chat_id", "supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4")
search_fields = ("name", "slug", "bale_chat_id", "supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4", "supervisor_mobile_5")
list_editable = ("sort_order", "show_in_customer_form", "accepts_external_tickets", "is_active")
filter_horizontal = ("permissions",)
inlines = (UnitMembershipInline,)
fieldsets = (
("مشخصات واحد", {"fields": ("name", "slug", "description", "search_keywords", "sort_order")}),
("اطلاع‌رسانی بله و سرپرستان", {"fields": ("bale_chat_id", "supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4")}),
("اطلاع‌رسانی بله و سرپرستان", {"fields": ("bale_chat_id", "supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4", "supervisor_mobile_5")}),
("نمایش و دسترسی", {"fields": ("permissions", "show_in_customer_form", "accepts_external_tickets", "is_active")}),
)
@@ -0,0 +1,13 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("staff", "0011_organizationalunit_supervisor_mobile_4")]
operations = [
migrations.AddField(
model_name="organizationalunit",
name="supervisor_mobile_5",
field=models.CharField(blank=True, max_length=16, verbose_name="شماره همراه سرپرست پنجم"),
),
]
+4 -2
View File
@@ -37,6 +37,7 @@ class OrganizationalUnit(PersianTextNormalizationMixin, TimeStampedModel):
supervisor_mobile_2 = models.CharField(max_length=16, blank=True, verbose_name="شماره همراه سرپرست دوم")
supervisor_mobile_3 = models.CharField(max_length=16, blank=True, verbose_name="شماره همراه سرپرست سوم")
supervisor_mobile_4 = models.CharField(max_length=16, blank=True, verbose_name="شماره همراه سرپرست چهارم")
supervisor_mobile_5 = models.CharField(max_length=16, blank=True, verbose_name="شماره همراه سرپرست پنجم")
class Meta:
ordering = ["sort_order", "name", "pk"]
@@ -54,6 +55,7 @@ class OrganizationalUnit(PersianTextNormalizationMixin, TimeStampedModel):
self.supervisor_mobile_2,
self.supervisor_mobile_3,
self.supervisor_mobile_4,
self.supervisor_mobile_5,
))))
def clean(self):
@@ -62,7 +64,7 @@ class OrganizationalUnit(PersianTextNormalizationMixin, TimeStampedModel):
normalized = []
errors = {}
for field_name in ("supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4"):
for field_name in ("supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4", "supervisor_mobile_5"):
value = getattr(self, field_name, "").strip()
if not value:
setattr(self, field_name, "")
@@ -82,7 +84,7 @@ class OrganizationalUnit(PersianTextNormalizationMixin, TimeStampedModel):
def save(self, *args, **kwargs):
from apps.accounts.models import normalize_mobile
for field_name in ("supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4"):
for field_name in ("supervisor_mobile_1", "supervisor_mobile_2", "supervisor_mobile_3", "supervisor_mobile_4", "supervisor_mobile_5"):
value = getattr(self, field_name, "").strip()
setattr(self, field_name, normalize_mobile(value) if value else "")
self.bale_chat_id = self.bale_chat_id.strip()
+2 -2
View File
@@ -9,9 +9,9 @@
.ticket-expert-picker{display:grid!important;grid-template-columns:1fr!important;gap:.55rem!important;margin:1rem 0!important}.ticket-expert-picker .expert-card{display:grid!important;min-height:0!important;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:.7rem;border-radius:1rem!important;padding:.72rem!important;text-align:right!important}.ticket-expert-picker .expert-card:has(input:checked){border-color:#0693e3;background:#e5f4ff}.ticket-expert-picker .expert-card img,.ticket-expert-picker .expert-avatar{width:2.8rem!important;height:2.8rem!important;margin:0!important;border-radius:.85rem!important}.ticket-expert-picker .expert-card strong,.ticket-expert-picker .expert-card small{display:block}.ticket-expert-picker .expert-card strong{font-size:.82rem}.ticket-expert-picker .expert-card small{margin-top:.1rem;font-size:.7rem;line-height:1.6}.ticket-expert-picker .expert-card:after{display:none;place-items:center;width:1.3rem;height:1.3rem;border-radius:50%;background:#0693e3;color:#fff;font-size:.6rem;font-weight:900;content:"✓"}.ticket-expert-picker .expert-card:has(input:checked):after{display:grid}
.ticket-attachment-zone{display:grid;gap:.7rem;margin-top:.8rem!important;border-color:rgba(8,55,107,.14)!important;border-radius:1rem!important;background:rgba(229,244,255,.45)!important;padding:.85rem!important;text-align:right!important}.ticket-attachment-head{display:flex;align-items:center;gap:.65rem}.ticket-attachment-head>span{display:grid;width:2.35rem;height:2.35rem;flex:none;place-items:center;border-radius:.72rem;background:#fff;color:#1977ce;font-size:1.05rem;font-weight:900}.ticket-attachment-head b,.ticket-attachment-head small{display:block}.ticket-attachment-head b{font-size:.82rem}.ticket-attachment-head small{margin-top:.1rem;color:rgba(8,55,107,.58);font-size:.7rem;line-height:1.65}.ticket-attachment-actions{display:grid!important;gap:.5rem!important}.ticket-attachment-actions label{display:grid;min-height:3rem;place-items:center;border:1px solid rgba(6,147,227,.38);border-radius:.9rem;background:#fff;color:#0b5f9b;font-size:.8rem;font-weight:900}.ticket-attachment-actions .gallery-button{background:#e5f4ff}.ticket-attachment-zone .compression-note{font-size:.7rem;line-height:1.65}.ticket-attachment-zone .file-chip{font-size:.72rem}.ticket-attachment-zone .file-chip button{min-width:1.7rem;min-height:1.7rem}.ticket-attachment-zone .canonical-files{display:none}
.ticket-voice-recorder{margin-top:.8rem;overflow:hidden;border:1px solid rgba(6,147,227,.2);border-radius:1.05rem;background:linear-gradient(145deg,#f7fcff,#edf7ff);box-shadow:0 12px 32px -28px rgba(8,55,107,.75)}.ticket-voice-head{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:.65rem;padding:.8rem}.ticket-voice-icon{display:grid;width:2.35rem;height:2.35rem;place-items:center;border-radius:.75rem;background:#fff;color:#0693e3;font-size:1rem}.ticket-voice-head b,.ticket-voice-head small,.ticket-voice-ready b,.ticket-voice-ready small{display:block}.ticket-voice-head b{font-size:.84rem}.ticket-voice-head small{margin-top:.12rem;color:rgba(8,55,107,.58);font-size:.72rem;line-height:1.7}.ticket-voice-timer{direction:ltr;border:1px solid rgba(6,147,227,.16);border-radius:999px;background:#fff;padding:.28rem .5rem;font-size:.75rem;font-weight:900;font-variant-numeric:tabular-nums}.ticket-voice-idle,.ticket-voice-live,.ticket-voice-preview{border-top:1px solid rgba(8,55,107,.07);padding:.75rem .8rem}.ticket-voice-idle{display:grid;gap:.35rem}.ticket-voice-idle>small{color:rgba(8,55,107,.58);font-size:.69rem;text-align:center}.ticket-voice-start{display:flex;min-height:3rem;align-items:center;justify-content:center;gap:.45rem;border:0;border-radius:.9rem;background:#0693e3;color:#fff;font-size:.82rem;font-weight:900}.ticket-voice-wave{display:flex;height:2.8rem;align-items:center;justify-content:center;gap:.24rem}.ticket-voice-wave i{width:.22rem;height:.75rem;border-radius:999px;background:#0693e3;animation:ticket-voice-wave .9s ease-in-out infinite}.ticket-voice-wave i:nth-child(2),.ticket-voice-wave i:nth-child(6){height:1.35rem;animation-delay:.12s}.ticket-voice-wave i:nth-child(3),.ticket-voice-wave i:nth-child(5){height:2rem;animation-delay:.24s}.ticket-voice-wave i:nth-child(4){height:2.5rem;animation-delay:.36s}.ticket-voice-recorder[data-state="paused"] .ticket-voice-wave i{animation-play-state:paused;opacity:.4}@keyframes ticket-voice-wave{0%,100%{transform:scaleY(.55)}50%{transform:scaleY(1)}}.ticket-voice-controls,.ticket-voice-preview-actions{display:grid;grid-template-columns:1fr 1.5fr;gap:.45rem}.ticket-voice-controls button,.ticket-voice-preview-actions button{min-height:2.65rem;border:1px solid rgba(8,55,107,.1);border-radius:.8rem;background:#fff;color:#08376b;font-size:.75rem;font-weight:850}.ticket-voice-controls .ticket-voice-stop{border-color:#0693e3;background:#0693e3;color:#fff}.ticket-voice-controls .ticket-voice-cancel{grid-column:1/-1;min-height:2rem;border:0;background:transparent;color:#9c3042}.ticket-voice-preview{display:grid;gap:.65rem;background:#fff}.ticket-voice-ready{display:flex;align-items:center;gap:.55rem}.ticket-voice-ready>span{display:grid;width:1.8rem;height:1.8rem;flex:none;place-items:center;border-radius:.55rem;background:#e5f8ef;color:#16815e;font-size:.65rem;font-weight:900}.ticket-voice-ready b{font-size:.8rem}.ticket-voice-ready small{color:rgba(8,55,107,.58);font-size:.7rem}.ticket-voice-preview audio,.ticket-review-audio{width:100%;height:2.6rem;accent-color:#0693e3}.ticket-voice-preview-actions{grid-template-columns:1fr 1fr}.ticket-voice-preview-actions [data-voice-delete]{border-color:rgba(169,39,60,.18);color:#a9273c}.ticket-voice-note{display:flex;gap:.35rem;margin:0;border-top:1px dashed rgba(8,55,107,.1);padding:.6rem .8rem;color:rgba(8,55,107,.58);font-size:.68rem;line-height:1.7}.ticket-voice-note span{color:#1977ce}.ticket-voice-error{margin:0;padding:.55rem .8rem;background:#fff0f1;color:#9c3042;font-size:.72rem;font-weight:800}
.ticket-server-errors:empty{display:none}.ticket-wizard-validation:empty{display:none}.ticket-wizard-validation.is-visible{position:relative;margin:.8rem 0 0;border:1px solid rgba(169,39,60,.2);border-radius:.85rem;background:#fff1f3;padding:.72rem 2.2rem .72rem .75rem;color:#92263a;font-size:.78rem;font-weight:850;line-height:1.8}.ticket-wizard-validation.is-visible:before{position:absolute;top:.75rem;right:.75rem;display:grid;width:1.15rem;height:1.15rem;place-items:center;border-radius:50%;background:#a9273c;color:#fff;font-size:.66rem;content:"!"}.ticket-wizard-validation.is-visible:focus{box-shadow:0 0 0 4px rgba(169,39,60,.13)}.ticket-wizard-page [aria-invalid="true"]{border-color:#b8324a!important;background:#fffafb!important;box-shadow:0 0 0 3px rgba(184,50,74,.1)!important}
.ticket-server-errors:empty{display:none}.ticket-wizard-validation:empty{display:none}.ticket-wizard-validation.is-visible{position:relative;margin:.8rem 0 0;border:1px solid rgba(169,39,60,.2);border-radius:.85rem;background:#fff1f3;padding:.72rem 2.2rem .72rem .75rem;color:#92263a;font-size:.78rem;font-weight:850;line-height:1.8}.ticket-wizard-validation.is-visible:before{position:absolute;top:.75rem;right:.75rem;display:grid;width:1.15rem;height:1.15rem;place-items:center;border-radius:50%;background:#a9273c;color:#fff;font-size:.66rem;content:"!"}.ticket-wizard-page [aria-invalid="true"]{border-color:#b8324a!important;background:#fffafb!important;box-shadow:0 0 0 3px rgba(184,50,74,.1)!important}.ticket-wizard-page .is-guided-field{position:relative;border-radius:1rem;box-shadow:0 0 0 4px rgba(6,147,227,.12);transition:box-shadow .2s ease}.ticket-wizard-page .is-guided-field:after{position:absolute;z-index:3;top:-.45rem;left:.6rem;border-radius:999px;background:#0679bd;padding:.18rem .48rem;color:#fff;font-size:.64rem;font-weight:850;content:"این بخش را کامل کنید";pointer-events:none}
.ticket-wizard-actions{position:sticky;z-index:10;bottom:4.8rem;display:flex;justify-content:flex-end;gap:.55rem;margin:1rem -1rem -1rem;border-top:1px solid rgba(8,55,107,.07);background:rgba(255,255,255,.97);padding:.7rem 1rem;backdrop-filter:blur(12px)}.ticket-wizard-actions>span{display:none}.ticket-wizard-actions button{min-height:3rem;border-radius:1rem;padding:.65rem .9rem;font-size:.82rem;font-weight:900}.ticket-wizard-actions .primary{border:0;background:#0693e3;color:#fff}.ticket-wizard-actions .secondary{border:1px solid rgba(8,55,107,.1);background:#fff;color:rgba(8,55,107,.72)}.ticket-wizard-card:not([data-wizard-enhanced]) [data-wizard-next],.ticket-wizard-card:not([data-wizard-enhanced]) [data-wizard-prev]{display:none}
.ticket-review-callout{display:flex;align-items:center;gap:.65rem;margin:.85rem 0 .65rem;border-radius:.9rem;background:#f0faf6;padding:.7rem}.ticket-review-callout>span{display:grid;width:2rem;height:2rem;flex:none;place-items:center;border-radius:.65rem;background:#fff;color:#16815e;font-weight:900}.ticket-review-callout b,.ticket-review-callout small{display:block}.ticket-review-callout b{font-size:.82rem}.ticket-review-callout small{margin-top:.1rem;color:#66877b;font-size:.7rem;line-height:1.65}.ticket-review-list{overflow:hidden;border:1px solid rgba(8,55,107,.1);border-radius:1rem;background:#fff}.ticket-review-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:start;gap:.6rem;padding:.75rem}.ticket-review-row+.ticket-review-row{border-top:1px solid rgba(8,55,107,.07)}.ticket-review-icon{display:grid;width:1.8rem;height:1.8rem;place-items:center;border-radius:.55rem;background:#e5f4ff;color:#1977ce;font-size:.65rem;font-weight:900}.ticket-review-content{min-width:0}.ticket-review-content>small{color:rgba(8,55,107,.58);font-size:.7rem;font-weight:800}.ticket-review-content h3{overflow-wrap:anywhere;margin:.16rem 0 .08rem;color:#08376b;font-size:.9rem;line-height:1.7}.ticket-review-content [data-review-subject]{font-size:1rem;font-weight:900;line-height:1.8}.ticket-review-content p{display:flex;flex-wrap:wrap;align-items:center;gap:.3rem;margin:0;color:rgba(8,55,107,.62);font-size:.72rem;line-height:1.8}.ticket-review-content p i{font-style:normal}.ticket-review-row>button{display:flex;min-height:2rem;align-items:center;gap:.25rem;border:0;border-radius:.65rem;background:#e5f4ff;padding:.35rem .5rem;color:#1977ce;font-size:.68rem;font-weight:900}.ticket-review-details .ticket-review-content{grid-column:2/-1}.ticket-review-details>button{grid-column:3;grid-row:1}.ticket-review-body{display:block!important;margin-top:.2rem!important;color:rgba(8,55,107,.82)!important;font-size:.88rem!important;font-weight:550;line-height:2!important;white-space:pre-wrap}.ticket-review-files{display:flex;flex-wrap:wrap;gap:.3rem;margin-top:.45rem}.ticket-review-files span{border-radius:999px;background:#e5f4ff;padding:.28rem .5rem;color:#1977ce;font-size:.68rem}.ticket-review-audio{margin-top:.55rem}.ticket-review-step{padding-bottom:6.75rem}.ticket-wizard-card[data-wizard-enhanced] .ticket-final-actions{position:fixed;z-index:45;right:.75rem;bottom:5rem;left:.75rem;display:grid;grid-template-columns:minmax(0,.78fr) minmax(0,1.4fr);gap:.5rem;margin:0;border:1px solid rgba(8,55,107,.12);border-radius:1.1rem;background:rgba(255,255,255,.96);padding:.55rem;box-shadow:0 20px 50px -20px rgba(8,55,107,.55);backdrop-filter:blur(14px)}
@media(min-width:640px){.ticket-wizard-page{padding-bottom:3rem}.ticket-wizard-topbar{min-height:3.8rem}.ticket-wizard-save{display:flex}.ticket-wizard-intro{margin:.8rem 0 1.35rem}.ticket-wizard-intro h1{font-size:2rem}.ticket-wizard-intro>p:last-child{font-size:.9rem}.ticket-wizard-progress{margin-bottom:1rem}.ticket-wizard-progress:before{top:1.12rem}.ticket-wizard-progress li{display:flex;justify-content:center;gap:.5rem}.ticket-wizard-progress li>span{width:2.3rem;height:2.3rem}.ticket-wizard-progress li>div{display:block}.ticket-wizard-progress li b,.ticket-wizard-progress li small{display:block}.ticket-wizard-progress li b{font-size:.78rem}.ticket-wizard-progress li small{font-size:.67rem}.ticket-wizard-card{border-radius:1.6rem}.ticket-wizard-step{padding:1.35rem 1.5rem}.ticket-wizard-step-head h2{font-size:1.2rem}.ticket-intent-search kbd{display:inline;border:1px solid rgba(8,55,107,.1);border-radius:.4rem;background:#eee;padding:.2rem .4rem;color:rgba(8,55,107,.58);font-size:.68rem}.ticket-routing-grid,.ticket-two-columns{grid-template-columns:1fr 1fr}.ticket-manual-body{padding:1rem!important}.ticket-expert-picker{grid-template-columns:repeat(3,1fr)!important}.ticket-expert-picker .expert-card{display:grid!important;grid-template-columns:1fr;justify-items:center;padding:.9rem!important;text-align:center!important}.ticket-expert-picker .expert-card img,.ticket-expert-picker .expert-avatar{width:3.3rem!important;height:3.3rem!important}.ticket-expert-picker .expert-card:after{position:absolute;top:.5rem;left:.5rem}.ticket-attachment-actions{grid-template-columns:1fr 1fr}.ticket-voice-idle{grid-template-columns:auto 1fr;align-items:center}.ticket-voice-idle>small{text-align:right}.ticket-voice-start{min-width:11rem}.ticket-voice-live{display:grid;grid-template-columns:1fr auto;align-items:center}.ticket-voice-controls{grid-template-columns:auto auto auto}.ticket-voice-controls .ticket-voice-cancel{grid-column:auto;min-height:2.65rem}.ticket-wizard-actions{position:static;margin:1.15rem -1.5rem -1.35rem;padding:.8rem 1.5rem}.ticket-wizard-actions>span{display:block;margin-left:auto;color:rgba(8,55,107,.45);font-size:.72rem}.ticket-wizard-actions button{min-width:8.5rem}.ticket-review-row{gap:.8rem;padding:.9rem 1rem}.ticket-review-details .ticket-review-content{grid-column:2}.ticket-review-content h3{font-size:.96rem}.ticket-review-content [data-review-subject]{font-size:1.08rem}.ticket-review-body{font-size:.94rem!important}.ticket-review-step{padding-bottom:6rem}.ticket-wizard-card[data-wizard-enhanced] .ticket-final-actions{position:fixed;right:auto;bottom:1rem;left:50%;width:min(calc(100% - 2rem),42rem);transform:translateX(-50%);padding:.6rem}}
@media(prefers-reduced-motion:reduce){.ticket-voice-wave i{animation:none}}
@media(prefers-reduced-motion:reduce){.ticket-voice-wave i{animation:none}.ticket-wizard-page .is-guided-field{transition:none}}
.ticket-wizard-credit{margin-top:-1px;border:1px solid rgba(8,55,107,.1);border-top:1px dashed rgba(8,55,107,.13);border-radius:0 0 1.15rem 1.15rem;background:#f8fbfd;padding:.85rem 1rem;color:rgba(8,55,107,.62);font-size:.74rem;line-height:1.9;text-align:center}.ticket-wizard-credit a{color:#1977ce}.ticket-wizard-card{border-bottom-right-radius:1.15rem;border-bottom-left-radius:1.15rem}@media(min-width:640px){.ticket-wizard-credit{border-radius:0 0 1.6rem 1.6rem}.ticket-wizard-card{border-bottom-right-radius:1.6rem;border-bottom-left-radius:1.6rem}}
+54 -11
View File
@@ -13,6 +13,8 @@
const canonicalFiles = form.querySelector('input[name="files"]');
const voiceRoot = form.querySelector('[data-ticket-voice-recorder]');
let currentStep = 1;
let guidedField = null;
let guideTimer = null;
const toPersianDigits = value => String(value).replace(/\d/g, digit => '۰۱۲۳۴۵۶۷۸۹'[digit]);
@@ -39,14 +41,47 @@
function clearValidation(number) {
const step = form.querySelector(`[data-wizard-step="${number}"]`);
const error = form.querySelector(`[data-wizard-error="${number}"]`);
step?.querySelectorAll('[aria-invalid="true"]').forEach(field => field.removeAttribute('aria-invalid'));
step?.querySelectorAll('[aria-invalid="true"]').forEach(field => {
field.removeAttribute('aria-invalid');
if (field.getAttribute('aria-describedby') === error?.id) field.removeAttribute('aria-describedby');
});
if (error) {
error.textContent = '';
error.classList.remove('is-visible');
error.removeAttribute('tabindex');
}
}
function clearGuidedField(field = guidedField) {
if (!field) return;
field.closest('.field, .ticket-intent-search, .ticket-location-fields')?.classList.remove('is-guided-field');
if (field === guidedField) guidedField = null;
if (guideTimer) {
window.clearTimeout(guideTimer);
guideTimer = null;
}
}
function guideToField(fields, error) {
const field = fields.find(item => item && !item.disabled && !item.hidden && item.getClientRects().length);
if (!field) {
error?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
return;
}
clearGuidedField();
guidedField = field;
const container = field.closest('.field, .ticket-intent-search, .ticket-location-fields');
container?.classList.add('is-guided-field');
requestAnimationFrame(() => {
field.focus({ preventScroll: true });
field.scrollIntoView({
behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
block: 'center',
inline: 'nearest',
});
});
guideTimer = window.setTimeout(() => clearGuidedField(field), 2800);
}
function showValidation(number, problem) {
const step = form.querySelector(`[data-wizard-step="${number}"]`);
const error = form.querySelector(`[data-wizard-error="${number}"]`);
@@ -55,14 +90,13 @@
if (manualRouting) manualRouting.open = true;
}
step?.querySelectorAll('[aria-invalid="true"]').forEach(field => field.removeAttribute('aria-invalid'));
problem.fields.forEach(field => field?.setAttribute('aria-invalid', 'true'));
error.textContent = problem.message;
error.tabIndex = -1;
error.classList.add('is-visible');
requestAnimationFrame(() => {
error.focus({ preventScroll: true });
error.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
problem.fields.forEach(field => {
field?.setAttribute('aria-invalid', 'true');
if (field && error?.id) field.setAttribute('aria-describedby', error.id);
});
error.textContent = problem.message;
error.classList.add('is-visible');
guideToField(problem.fields, error);
}
function problemForStep(number) {
@@ -145,9 +179,15 @@
form.addEventListener('input', event => {
event.target.removeAttribute?.('aria-invalid');
if (event.target.getAttribute?.('aria-describedby')?.startsWith('wizard-error-')) event.target.removeAttribute('aria-describedby');
clearGuidedField(event.target);
if (event.target === body) form.querySelector('[data-body-count]').textContent = toPersianDigits(body.value.length);
});
form.addEventListener('change', event => event.target.removeAttribute?.('aria-invalid'));
form.addEventListener('change', event => {
event.target.removeAttribute?.('aria-invalid');
if (event.target.getAttribute?.('aria-describedby')?.startsWith('wizard-error-')) event.target.removeAttribute('aria-describedby');
clearGuidedField(event.target);
});
form.addEventListener('submit', event => {
const invalidStep = [1, 2, 3, 4].find(number => problemForStep(number));
@@ -314,7 +354,10 @@
showStep(initialStep, { scroll: false });
if (serverErrorStep) {
const messages = [...serverErrorStep.querySelectorAll('.errorlist li')].map(item => item.textContent.trim()).filter(Boolean);
const fields = [...serverErrorStep.querySelectorAll('.errorlist')]
.map(list => list.closest('.field, .ticket-server-errors')?.querySelector('input, select, textarea'))
.filter(Boolean);
const number = Number(serverErrorStep.dataset.wizardStep);
requestAnimationFrame(() => showValidation(number, { message: messages.join(' '), fields: [] }));
requestAnimationFrame(() => showValidation(number, { message: messages.join(' '), fields }));
}
})();
+1 -1
View File
@@ -61,7 +61,7 @@
{% 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 %}
{% block pwa_install_prompt %}{% endblock %}
<script src="{% static 'js/voice-recorder.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/ui.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/chat-coach.js' %}?v={{ static_asset_version }}"></script>
{% if user.is_authenticated %}<script defer src="{% static 'js/pwa.js' %}?v={{ static_asset_version }}"></script>{% endif %}
{% block extra_scripts %}{% endblock %}
+1
View File
@@ -26,3 +26,4 @@
{% endfor %}
</div>
{% endblock %}
{% block pwa_install_prompt %}<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>{% endblock %}
+5 -5
View File
@@ -62,7 +62,7 @@
{{ form.subject }}{{ form.subject.errors }}
<small>این عنوان فقط توسط شما نوشته می‌شود و با تغییر واحد یا دسته‌بندی عوض نخواهد شد.</small>
</div>
<p class="ticket-wizard-validation" data-wizard-error="1" role="alert"></p>
<p class="ticket-wizard-validation" id="wizard-error-1" data-wizard-error="1" role="alert"></p>
<footer class="ticket-wizard-actions"><span>حدود یک دقیقه تا پایان</span><button class="primary" type="button" data-wizard-next>ادامه <b>←</b></button></footer>
</section>
@@ -76,7 +76,7 @@
<p class="location-hint" data-location-hint>نام استان را جست‌وجو و انتخاب کنید.</p>
</section>
<div class="ticket-soft-tip"><span>i</span><p><b>نیازی به انتخاب ندارید</b> این بخش کاملاً اختیاری است و می‌توانید مستقیماً ادامه دهید.</p></div>
<p class="ticket-wizard-validation" data-wizard-error="2" role="alert"></p>
<p class="ticket-wizard-validation" id="wizard-error-2" data-wizard-error="2" role="alert"></p>
<footer class="ticket-wizard-actions"><button class="secondary" type="button" data-wizard-prev>مرحله قبل</button><button class="primary" type="button" data-wizard-next>ادامه <b>←</b></button></footer>
</section>
@@ -91,7 +91,7 @@
{% 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 }}
<p class="ticket-wizard-validation" data-wizard-error="3" role="alert"></p>
<p class="ticket-wizard-validation" id="wizard-error-3" data-wizard-error="3" role="alert"></p>
<footer class="ticket-wizard-actions"><button class="secondary" type="button" data-wizard-prev>مرحله قبل</button><button class="primary" type="button" data-wizard-next>ادامه <b>←</b></button></footer>
</section>
@@ -120,7 +120,7 @@
</section>
<div class="ticket-server-errors" data-step-server-errors>{% if form.non_field_errors %}{{ form.non_field_errors }}{% endif %}</div>
<p class="ticket-wizard-validation" data-wizard-error="4" role="alert"></p>
<p class="ticket-wizard-validation" id="wizard-error-4" data-wizard-error="4" role="alert"></p>
<footer class="ticket-wizard-actions"><button class="secondary" type="button" data-wizard-prev>مرحله قبل</button><button class="primary" type="button" data-wizard-next>مشاهده پیش‌نمایش <b>←</b></button></footer>
</section>
@@ -133,7 +133,7 @@
<section class="ticket-review-row"><span class="ticket-review-icon">۳</span><div class="ticket-review-content"><small>نحوه ارجاع</small><h3 data-review-expert>صف عمومی</h3><p>در همان واحد سازمانی</p></div><button type="button" data-wizard-edit="3"><span>✎</span> ویرایش</button></section>
<section class="ticket-review-row ticket-review-details"><span class="ticket-review-icon">۴</span><div class="ticket-review-content"><small>شرح و پیوست‌ها</small><p class="ticket-review-body" data-review-body>—</p><div class="ticket-review-files" data-review-files></div><audio class="ticket-review-audio" controls hidden data-review-audio></audio></div><button type="button" data-wizard-edit="4"><span>✎</span> ویرایش</button></section>
</div>
<p class="ticket-wizard-validation" data-wizard-error="5" role="alert"></p>
<p class="ticket-wizard-validation" id="wizard-error-5" data-wizard-error="5" role="alert"></p>
<footer class="ticket-wizard-actions ticket-final-actions"><button class="secondary" type="button" data-wizard-prev>مرحله قبل</button><button class="primary" type="submit" data-final-submit>ثبت و ارسال نهایی <b>←</b></button></footer>
</section>
</form>
@@ -180,10 +180,14 @@ def test_e2e_customer_wizard_validation_preview_edit_and_floating_submit(
expect(error).to_be_in_viewport()
expect(page.locator('[data-ticket-subject]')).to_have_attribute("aria-invalid", "true")
expect(form.category).to_be_visible()
assert page.evaluate("document.activeElement === document.querySelector('[data-wizard-error=\"1\"]')")
assert page.evaluate("document.activeElement.matches('[aria-invalid=\"true\"]:not(:disabled)')")
expect(page.locator(".is-guided-field")).to_have_count(1)
form.select_search_result("عمومی فروش", "عمومی فروش")
expect(page.locator('[data-ticket-subject]')).to_have_value("")
page.locator('[data-wizard-step="1"] [data-wizard-next]').click()
assert page.evaluate("document.activeElement === document.querySelector('[data-ticket-subject]')")
expect(page.locator('.ticket-subject-field')).to_have_class(re.compile(r"is-guided-field"))
page.locator('[data-ticket-subject]').fill(subject)
expect(page.locator('[data-ticket-subject]')).to_have_value(subject)
page.locator('[data-wizard-step="1"] [data-wizard-next]').click()
+3 -1
View File
@@ -142,10 +142,11 @@ def test_unit_notification_destinations_normalize_and_reject_duplicate_superviso
supervisor_mobile_1="0912 111 2233",
supervisor_mobile_2="0935 222 3344",
supervisor_mobile_4="0990 444 5566",
supervisor_mobile_5="0921 555 6677",
)
unit.full_clean()
unit.save()
assert unit.supervisor_mobiles == ("+989121112233", "+989352223344", "+989904445566")
assert unit.supervisor_mobiles == ("+989121112233", "+989352223344", "+989904445566", "+989215556677")
unit.supervisor_mobile_3 = "09121112233"
with pytest.raises(ValidationError, match="قبلاً"):
@@ -161,6 +162,7 @@ def test_new_ticket_sms_is_queued_for_each_unit_supervisor_only_on_creation(cate
unit.supervisor_mobile_2 = "09352223344"
unit.supervisor_mobile_3 = "09903334455"
unit.supervisor_mobile_4 = "09214445566"
unit.supervisor_mobile_5 = "09195556677"
unit.save()
ticket = create_ticket(customer=customer, category=category, subject=" پیگیری بارگیری ", body="متن")
jobs = list(NotificationJob.objects.filter(ticket=ticket, event_type="UNIT_SUPERVISOR_NEW_TICKET").order_by("id"))
+9
View File
@@ -529,6 +529,15 @@ def test_pwa_prompt_is_mobile_only_and_uses_square_app_icon(client):
assert "mobileDevice" in script and "banner&&mobileDevice" in script
assert all(icon["src"].endswith("pwa-icon.png") for icon in manifest["icons"])
@pytest.mark.django_db
def test_pwa_install_prompt_is_only_rendered_on_customer_ticket_list(client):
customer=User.objects.create_user(username="pwa-list-only",mobile="09120000123",account_type="CUSTOMER")
client.force_login(customer)
assert 'data-pwa-install' in client.get(reverse("tickets:list")).content.decode()
assert 'data-pwa-install' not in client.get(reverse("tickets:new")).content.decode()
assert 'data-pwa-install' not in Path("templates/base.html").read_text(encoding="utf-8")
@pytest.mark.django_db
def test_customer_list_recommends_profile_completion_without_blocking_ticket_creation(client):
customer=User.objects.create_user(username="incomplete-profile",mobile="09202202129",account_type="CUSTOMER")