From d72381f0ac392ad45770416ef66b5ea9fd57a549 Mon Sep 17 00:00:00 2001 From: javad Date: Wed, 16 Sep 2026 11:06:38 +0330 Subject: [PATCH] fix pwa, wizard focus, supervisor 5 --- apps/staff/admin.py | 4 +- ..._organizationalunit_supervisor_mobile_5.py | 13 ++++ apps/staff/models.py | 6 +- static/css/customer-ticket-wizard.css | 4 +- static/js/customer-ticket-wizard.js | 65 +++++++++++++++---- templates/base.html | 2 +- templates/tickets/customer_list.html | 1 + templates/tickets/customer_new.html | 10 +-- tests/e2e/test_public_ticket_workflows_e2e.py | 6 +- tests/test_domain.py | 4 +- tests/test_views.py | 9 +++ 11 files changed, 99 insertions(+), 25 deletions(-) create mode 100644 apps/staff/migrations/0012_organizationalunit_supervisor_mobile_5.py diff --git a/apps/staff/admin.py b/apps/staff/admin.py index 834f555..cdce5c5 100644 --- a/apps/staff/admin.py +++ b/apps/staff/admin.py @@ -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")}), ) diff --git a/apps/staff/migrations/0012_organizationalunit_supervisor_mobile_5.py b/apps/staff/migrations/0012_organizationalunit_supervisor_mobile_5.py new file mode 100644 index 0000000..b54b032 --- /dev/null +++ b/apps/staff/migrations/0012_organizationalunit_supervisor_mobile_5.py @@ -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="شماره همراه سرپرست پنجم"), + ), + ] diff --git a/apps/staff/models.py b/apps/staff/models.py index c21128a..fb09a4f 100644 --- a/apps/staff/models.py +++ b/apps/staff/models.py @@ -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() diff --git a/static/css/customer-ticket-wizard.css b/static/css/customer-ticket-wizard.css index 6b70170..17deec2 100644 --- a/static/css/customer-ticket-wizard.css +++ b/static/css/customer-ticket-wizard.css @@ -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}} diff --git a/static/js/customer-ticket-wizard.js b/static/js/customer-ticket-wizard.js index 05999eb..60c5be8 100644 --- a/static/js/customer-ticket-wizard.js +++ b/static/js/customer-ticket-wizard.js @@ -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 })); } })(); diff --git a/templates/base.html b/templates/base.html index b82e163..78490e7 100644 --- a/templates/base.html +++ b/templates/base.html @@ -61,7 +61,7 @@ {% elif user.is_authenticated %} {% endif %} -{% if user.is_authenticated %}{% endif %} +{% block pwa_install_prompt %}{% endblock %} {% if user.is_authenticated %}{% endif %} {% block extra_scripts %}{% endblock %} diff --git a/templates/tickets/customer_list.html b/templates/tickets/customer_list.html index 4e52a61..2bac4fc 100644 --- a/templates/tickets/customer_list.html +++ b/templates/tickets/customer_list.html @@ -26,3 +26,4 @@ {% endfor %} {% endblock %} +{% block pwa_install_prompt %}{% endblock %} diff --git a/templates/tickets/customer_new.html b/templates/tickets/customer_new.html index 6c3c56c..bf6bef6 100644 --- a/templates/tickets/customer_new.html +++ b/templates/tickets/customer_new.html @@ -62,7 +62,7 @@ {{ form.subject }}{{ form.subject.errors }} این عنوان فقط توسط شما نوشته می‌شود و با تغییر واحد یا دسته‌بندی عوض نخواهد شد. - + @@ -76,7 +76,7 @@

نام استان را جست‌وجو و انتخاب کنید.

i

نیازی به انتخاب ندارید این بخش کاملاً اختیاری است و می‌توانید مستقیماً ادامه دهید.

- + @@ -91,7 +91,7 @@ {% for expert in form.fields.selected_expert.queryset %}{% empty %}

کارشناس قابل انتخابی ثبت نشده؛ درخواست به صف عمومی می‌رود.

{% endfor %} {{ form.selected_expert.errors }} - + @@ -120,7 +120,7 @@
{% if form.non_field_errors %}{{ form.non_field_errors }}{% endif %}
- + @@ -133,7 +133,7 @@
۳
نحوه ارجاع

صف عمومی

در همان واحد سازمانی

۴
شرح و پیوست‌ها

—

- + diff --git a/tests/e2e/test_public_ticket_workflows_e2e.py b/tests/e2e/test_public_ticket_workflows_e2e.py index 9035c91..1f3e303 100644 --- a/tests/e2e/test_public_ticket_workflows_e2e.py +++ b/tests/e2e/test_public_ticket_workflows_e2e.py @@ -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() diff --git a/tests/test_domain.py b/tests/test_domain.py index ed329de..67164e5 100644 --- a/tests/test_domain.py +++ b/tests/test_domain.py @@ -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")) diff --git a/tests/test_views.py b/tests/test_views.py index 80e2e33..0b02739 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -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")