fix focus on ticket new, sitebranding, customer edit error

This commit is contained in:
javad committed 2026-09-16 12:44:43 +03:30
1 parent d72381f0ac
commit 6b841dc5fa
11 files changed
+139 -10

No files matched your search

+19 -3
View File
@@ -682,9 +682,19 @@ def _visible_dispatches(user):
def _contact_queryset(user):
"""Contacts visible through at least one of the staff member's units."""
"""Contacts visible through a permitted group or a viewable ticket.
A customer created by login or ticket intake may not have a contact group
yet. Keeping ticket-visible customers in this queryset makes the contact
list and its edit endpoint use the same visibility rule.
"""
visible_groups = ContactGroup.objects.filter(unit_id__in=_unit_ids_with_permission(user, "customers.view_customerprofile")).select_related("unit")
return CustomerProfile.objects.filter(groups__in=visible_groups).select_related("user").prefetch_related(Prefetch("groups", queryset=visible_groups, to_attr="visible_groups")).distinct()
visible_customer_ids = staff_ticket_rows(user).values_list("customer_id", flat=True)
return CustomerProfile.objects.filter(
Q(groups__in=visible_groups) | Q(user_id__in=visible_customer_ids)
).select_related("user").prefetch_related(
Prefetch("groups", queryset=visible_groups, to_attr="visible_groups")
).distinct()
def _has_any_unit_permission(user, permission):
@@ -747,7 +757,13 @@ def staff_contact_create(request):
def staff_contact_edit(request, public_id):
profile = get_object_or_404(_contact_queryset(request.user), user__public_id=public_id)
manageable_unit_ids = _unit_ids_with_permission(request.user, "customers.change_customerprofile")
if not set(profile.groups.values_list("unit_id", flat=True)) & manageable_unit_ids:
editable_through_group = profile.groups.filter(unit_id__in=manageable_unit_ids).exists()
editable_through_ticket = staff_ticket_rows(request.user).filter(
Q(assigned_unit_id__in=manageable_unit_ids)
| Q(assigned_unit__isnull=True, owning_unit_id__in=manageable_unit_ids),
customer_id=profile.user_id,
).exists()
if not (editable_through_group or editable_through_ticket):
raise PermissionDenied
form = StaffContactForm(request.POST or None, user=request.user, instance=profile, unit_ids=manageable_unit_ids)
if request.method == "POST" and form.is_valid():
+1 -1
View File
@@ -2,7 +2,7 @@
.ticket-wizard-topbar{display:flex;min-height:3rem;align-items:center;justify-content:space-between;gap:1rem}.ticket-wizard-back{color:#1977ce;font-size:.82rem;font-weight:850}.ticket-wizard-save{display:none;align-items:center;gap:.45rem;color:rgba(8,55,107,.62);font-size:.76rem}.ticket-wizard-save i{width:.48rem;height:.48rem;border-radius:50%;background:#19a774;box-shadow:0 0 0 .23rem rgba(25,167,116,.1)}
.ticket-wizard-intro{margin:.65rem 0 1.1rem}.ticket-wizard-intro .page-kicker{margin:0;color:#0693e3;font-size:.76rem;font-weight:900}.ticket-wizard-intro h1{margin:.25rem 0;font-size:1.5rem;line-height:1.5}.ticket-wizard-intro>p:last-child{margin:0;color:rgba(8,55,107,.62);font-size:.86rem;line-height:1.9}
.ticket-wizard-progress{position:relative;display:grid;grid-template-columns:repeat(5,1fr);margin:0 0 .8rem;padding:0;list-style:none}.ticket-wizard-progress:before{position:absolute;top:1.05rem;right:10%;left:10%;height:2px;background:rgba(8,55,107,.1);content:""}.ticket-wizard-progress li{position:relative;display:grid;justify-items:center;color:rgba(8,55,107,.35)}.ticket-wizard-progress li>span{z-index:1;display:grid;width:2.1rem;height:2.1rem;place-items:center;border:2px solid #d5dfe4;border-radius:.7rem;background:#eee;font-size:.72rem;font-weight:900}.ticket-wizard-progress li>div{display:none}.ticket-wizard-progress li.is-active>span{border-color:#0693e3;background:#0693e3;color:#fff;box-shadow:0 8px 20px -12px #0693e3}.ticket-wizard-progress li.is-complete>span{border-color:#27a47a;background:#e5f7f0;color:#16815e}
.ticket-wizard-card{overflow:visible;border:1px solid rgba(8,55,107,.1);border-radius:1.15rem;background:#fff;box-shadow:0 18px 55px -35px rgba(8,55,107,.35)}.ticket-wizard-step{padding:1rem}.ticket-wizard-card[data-wizard-enhanced] .ticket-wizard-step:not(.is-active){display:none}.ticket-wizard-step-head{display:flex;align-items:flex-start;gap:.7rem;border-bottom:1px solid rgba(8,55,107,.07);padding-bottom:1rem}.ticket-wizard-symbol{display:grid;width:2.45rem;height:2.45rem;flex:none;place-items:center;border-radius:.75rem;background:#e5f4ff;color:#1977ce;font-size:1.05rem;font-weight:900}.ticket-wizard-symbol.is-success{background:#e5f8ef;color:#16815e}.ticket-wizard-step-head small{color:rgba(8,55,107,.48);font-size:.7rem;font-weight:750}.ticket-wizard-step-head h2{margin:.08rem 0;color:#08376b;font-size:1.08rem;line-height:1.6}.ticket-wizard-step-head p{margin:0;color:rgba(8,55,107,.62);font-size:.82rem;line-height:1.85}
.ticket-wizard-card{overflow:visible;scroll-margin-top:.75rem;border:1px solid rgba(8,55,107,.1);border-radius:1.15rem;background:#fff;box-shadow:0 18px 55px -35px rgba(8,55,107,.35)}.ticket-wizard-card:focus{outline:none}.ticket-wizard-step{padding:1rem}.ticket-wizard-card[data-wizard-enhanced] .ticket-wizard-step:not(.is-active){display:none}.ticket-wizard-step-head{display:flex;align-items:flex-start;gap:.7rem;border-bottom:1px solid rgba(8,55,107,.07);padding-bottom:1rem}.ticket-wizard-symbol{display:grid;width:2.45rem;height:2.45rem;flex:none;place-items:center;border-radius:.75rem;background:#e5f4ff;color:#1977ce;font-size:1.05rem;font-weight:900}.ticket-wizard-symbol.is-success{background:#e5f8ef;color:#16815e}.ticket-wizard-step-head small{color:rgba(8,55,107,.48);font-size:.7rem;font-weight:750}.ticket-wizard-step-head h2{margin:.08rem 0;color:#08376b;font-size:1.08rem;line-height:1.6}.ticket-wizard-step-head p{margin:0;color:rgba(8,55,107,.62);font-size:.82rem;line-height:1.85}
.ticket-intent-search{display:flex;align-items:center;margin:1rem 0 .65rem;border:1px solid rgba(8,55,107,.14);border-radius:1rem;background:#fff;padding:0 .75rem;box-shadow:0 1px 3px rgba(8,55,107,.06)}.ticket-intent-search:focus-within{border-color:#0693e3;box-shadow:0 0 0 4px rgba(6,147,227,.1)}.ticket-intent-search i{color:#0693e3;font-style:normal}.ticket-intent-search input{min-height:3.2rem;border:0!important;background:transparent!important;padding:.7rem!important;font-size:1rem!important;box-shadow:none!important}.ticket-intent-search kbd{display:none}.ticket-intent-results:empty{display:none}.ticket-intent-results{margin-bottom:.65rem}.ticket-intent-results .intent-result{font-size:.84rem}.ticket-intent-results .intent-result span{font-size:.75rem}.ticket-intent-results .intent-result small,.ticket-intent-results .intent-message{font-size:.73rem;line-height:1.7}
.ticket-manual-routing{margin:.75rem 0!important;border-color:rgba(8,55,107,.1)!important;border-radius:1rem!important}.ticket-manual-routing summary{background:#fafcfd!important;padding:.8rem!important;font-size:.8rem;font-weight:850}.ticket-manual-routing summary small{font-size:.7rem!important}.ticket-manual-body{padding:.8rem!important}.ticket-routing-grid,.ticket-two-columns{display:grid;gap:.7rem}.ticket-wizard-page .field{margin-bottom:.8rem}.ticket-wizard-page label{font-size:.82rem}.ticket-wizard-page input:not([type=checkbox]):not([type=radio]),.ticket-wizard-page textarea,.ticket-wizard-page select{min-height:3.1rem;font-size:1rem}.ticket-subject-field{margin-top:.85rem!important}.ticket-subject-field label em{color:#fa9f00;font-style:normal}.ticket-subject-field>small,.ticket-body-field>small{display:flex;justify-content:flex-end;margin-top:.35rem;color:rgba(8,55,107,.5);font-size:.7rem;font-weight:500}.ticket-body-field textarea{min-height:9rem;line-height:1.9}.ticket-wizard-page .errorlist{margin:.35rem 0 0;padding:.5rem .65rem;border-radius:.7rem;background:#fff1f3;color:#92263a;font-size:.74rem;font-weight:750;list-style:none}
.ticket-location-fields{margin-top:1rem}.ticket-location-fields .location-hint{margin:.2rem 0;color:rgba(8,55,107,.55);font-size:.72rem}.ticket-soft-tip{display:flex;align-items:center;gap:.65rem;margin:.8rem 0;border-radius:.85rem;background:#e5f4ff;padding:.75rem;color:rgba(8,55,107,.68)}.ticket-soft-tip>span{display:grid;width:1.75rem;height:1.75rem;flex:none;place-items:center;border-radius:.55rem;background:#fff;color:#1977ce;font-weight:900}.ticket-soft-tip p{margin:0;font-size:.76rem;line-height:1.75}.ticket-soft-tip b{color:#08376b}
+1 -1
View File
@@ -1,2 +1,2 @@
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-leading:initial;--tw-font-weight:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-tracking:initial;--tw-border-style:solid;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:Vazirmatn,ui-sans-serif,system-ui;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-800:oklch(47.3% .137 46.201);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-800:oklch(43.2% .095 166.913);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-400:oklch(70.4% .04 256.788);--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--blur-sm:8px;--blur-xl:24px;--blur-2xl:40px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink:#08376b;--color-ink-soft:#0b4c8c;--color-cream:#eee;--color-soft:#eaeaea;--color-paper:#fff;--color-sea:#0693e3;--color-sea-dark:#1977ce;--color-blue:#1977ce;--color-mint:#e5f4ff;--color-coral:#fa9f00;--color-sun:#ffc451;--shadow-panel:0 18px 55px -35px #08376b59}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-Line truncated
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-leading:initial;--tw-font-weight:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-tracking:initial;--tw-border-style:solid;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:Vazirmatn,ui-sans-serif,system-ui;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-800:oklch(47.3% .137 46.201);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-800:oklch(43.2% .095 166.913);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-400:oklch(70.4% .04 256.788);--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--blur-sm:8px;--blur-xl:24px;--blur-2xl:40px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink:#08376b;--color-ink-soft:#0b4c8c;--color-cream:#eee;--color-soft:#eaeaea;--color-paper:#fff;--color-sea:#0693e3;--color-sea-dark:#1977ce;--color-blue:#1977ce;--color-mint:#e5f4ff;--color-coral:#fa9f00;--color-sun:#ffc451;--shadow-panel:0 18px 55px -35px #08376b59}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-Line truncated
+14 -1
View File
@@ -349,6 +349,19 @@
setupVoiceRecorder(voiceRoot);
form.dataset.wizardEnhanced = '';
function focusWizardCard() {
requestAnimationFrame(() => {
// Do not steal focus if the visitor has already started interacting.
if (document.activeElement !== document.body && document.activeElement !== document.documentElement) return;
form.focus({ preventScroll: true });
form.scrollIntoView({
behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
block: 'start',
inline: 'nearest',
});
});
}
const serverErrorStep = steps.find(step => step.querySelector('.errorlist'));
const initialStep = serverErrorStep ? Number(serverErrorStep.dataset.wizardStep) : 1;
showStep(initialStep, { scroll: false });
@@ -359,5 +372,5 @@
.filter(Boolean);
const number = Number(serverErrorStep.dataset.wizardStep);
requestAnimationFrame(() => showValidation(number, { message: messages.join(' '), fields }));
}
} else focusWizardCard();
})();
+1
View File
@@ -92,6 +92,7 @@ body:has(.ticket-workspace) .flash{position:fixed;top:5rem;left:50%;z-index:100;
.site-footer{display:flex;min-height:2rem;align-items:center;justify-content:center;gap:.35rem;padding:.4rem 1rem;color:#60758b;font-size:.68rem;font-weight:500;text-align:center}
.staff-main>.site-footer{border-top:1px solid rgba(8,55,107,.06)}
body:has(.ticket-workspace) .site-footer{display:none}body:has(.ticket-workspace) .ticket-workspace{height:calc(100dvh - 4.25rem)}body:has(.staff-ticket-workspace) .ticket-workspace{height:calc(100dvh - 4.5rem)}
.customer-workspace.has-ticket-detail-credit{display:grid;grid-template-rows:auto minmax(0,1fr) auto}.customer-workspace.has-ticket-detail-credit .workspace-grid{height:auto;min-height:0}.ticket-detail-credit{display:flex;min-height:2rem;align-items:center;justify-content:center;border-top:1px solid rgba(8,55,107,.08);background:var(--color-paper);padding:.35rem 1rem;color:#60758b;font-size:.68rem;font-weight:500;line-height:1.6;text-align:center}.ticket-detail-credit a{color:var(--color-sea)}
@media(max-width:639px){.logo-copy{font-size:.82rem}.header-company-logo{width:4.75rem;max-height:2.1rem}.auth-brand{gap:.5rem}.auth-brand img{width:7rem;max-height:3.1rem}.auth-brand>div{min-width:0}.auth-footer{font-size:.6rem}.ticket-company-logo{width:2.5rem;max-height:1.1rem}.site-footer{padding-inline:3.5rem;font-size:.62rem;line-height:1.6}body:has(.ticket-workspace) .ticket-workspace{height:100dvh}body:has(.staff-ticket-workspace) .ticket-workspace{height:calc(100dvh - 4.5rem)}}
/* Session actions, staff-created tickets and install prompt */
+3 -1
View File
@@ -2,7 +2,7 @@
{% block title %}{{ page_title }}{% endblock %}
{% block content %}
{% if magic_link_access %}<div class="flash" role="status">شما از طریق لینک امن، فقط به همین تیکت دسترسی دارید. برای مشاهده سایر درخواست‌ها و پروفایل، با شماره موبایل خود وارد شوید.</div>{% endif %}
<div class="ticket-workspace customer-workspace" data-conversation-workspace data-poll-url="{% url 'tickets:poll' ticket.public_id %}" data-read-url="{% url 'tickets:read' ticket.public_id %}" data-poll-version="{{ poll_version }}" data-poll-cursor="{{ poll_cursor }}" data-draft-key="ticket-{{ ticket.public_id }}-customer" data-reply-result="{{ request.GET.reply_result|default:'' }}">
<div class="ticket-workspace customer-workspace{% if magic_link_access %} has-ticket-detail-credit{% endif %}" data-conversation-workspace data-poll-url="{% url 'tickets:poll' ticket.public_id %}" data-read-url="{% url 'tickets:read' ticket.public_id %}" data-poll-version="{{ poll_version }}" data-poll-cursor="{{ poll_cursor }}" data-draft-key="ticket-{{ ticket.public_id }}-customer" data-reply-result="{{ request.GET.reply_result|default:'' }}">
<header class="ticket-workspace-head"><div class="ticket-title"><a class="workspace-back" href="{% if magic_link_access %}{% url 'accounts:login' %}{% else %}{% url 'tickets:list' %}{% endif %}" aria-label="بازگشت">→</a><div><span class="ticket-number" dir="ltr">{{ ticket.ticket_number }}</span><h1>{{ ticket.subject }}</h1></div></div><div class="ticket-head-actions"><span class="badge" data-ticket-status>{{ ticket.get_status_display }}</span><button type="button" class="icon-button" data-toggle-panel="ticket-info" aria-label="جزئیات تیکت">ⓘ</button></div></header>
<div class="workspace-grid customer-grid">
<main class="conversation-pane">
@@ -22,7 +22,9 @@
</main>
<aside class="workspace-side" id="ticket-info" data-workspace-panel><button type="button" class="panel-close" data-close-panel aria-label="بستن">×</button><section class="info-card"><p class="panel-index">جزئیات تیکت</p><div class="info-list"><div class="info-row"><span>دسته‌بندی</span><b>{{ ticket.category }}</b></div><div class="info-row"><span>موقعیت</span><b>{% if ticket.city %}{{ ticket.city }}، {{ ticket.province }}{% elif ticket.province %}{{ ticket.province }}{% else %}ثبت نشده{% endif %}</b></div><div class="info-row"><span>کارشناس</span><b>{{ ticket.assigned_expert_name }}</b></div><div class="info-row"><span>آخرین فعالیت</span><b>{{ ticket.last_activity_at|jalali_datetime }}</b></div></div></section><section class="info-card">{% if magic_link_access %}<h3>مشاهده همه درخواست‌ها</h3><p class="muted mt-2">برای دسترسی به پروفایل و سایر درخواست‌ها، ابتدا با شماره موبایل خود وارد شوید.</p><a class="back-link mt-3" href="{% url 'accounts:login' %}">ورود با شماره موبایل ←</a>{% else %}<h3>موضوع دیگری دارید؟</h3><p class="muted mt-2">برای هر موضوع یک درخواست جدا بسازید تا پیگیری منظم بماند.</p><a class="back-link mt-3" href="{% url 'tickets:new' %}">ثبت درخواست تازه ←</a>{% endif %}</section></aside>
</div>
{% if magic_link_access %}<footer class="ticket-detail-credit">{{ developer_credit }}</footer>{% endif %}
</div>
{% include 'tickets/_image_viewer.html' %}
<script src="{% static 'vendor/compressor.min.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/attachment-uploader.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/image-viewer.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/conversation.js' %}?v={{ static_asset_version }}"></script>
{% endblock %}
{% block footer %}{% endblock %}
+1 -1
View File
@@ -25,7 +25,7 @@
<li data-wizard-progress-item="5"><span>۵</span><div><b>بازبینی</b><small>تأیید نهایی</small></div></li>
</ol>
<form class="ticket-wizard-card" method="post" enctype="multipart/form-data" novalidate
<form class="ticket-wizard-card" method="post" enctype="multipart/form-data" novalidate tabindex="-1" aria-label="فرم ثبت درخواست"
data-ticket-composer data-ticket-wizard
data-unit-options-url="{% url 'tickets:new_options' %}"
data-ticket-search-url="{% url 'tickets:new_search' %}"
+2
View File
@@ -12,6 +12,8 @@ class CustomerNewTicketPage(BasePage):
def open(self):
self.goto("/tickets/new/")
if not self.unit.is_visible():
self.page.locator("details.ticket-manual-routing summary").click()
expect(self.unit).to_be_visible()
def select_unit(self, unit_id: int, expected_category: str):
@@ -170,6 +170,10 @@ def test_e2e_customer_wizard_validation_preview_edit_and_floating_submit(
with browser_page() as page:
page.set_viewport_size({"width": 390, "height": 640})
form = open_form(page, live_server, customer)
wizard = page.locator('[data-ticket-wizard]')
expect(wizard).to_be_focused()
expect(wizard).to_be_in_viewport()
assert wizard.bounding_box()["y"] < page.viewport_size["height"] / 3
expect(page.locator('[data-wizard-step="1"]')).to_be_visible()
expect(page.locator('[data-wizard-step="2"]')).to_be_hidden()
expect(page.locator('[data-ticket-search-results]')).to_be_hidden()
@@ -186,7 +190,7 @@ def test_e2e_customer_wizard_validation_preview_edit_and_floating_submit(
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('[data-ticket-subject]')).to_be_focused()
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)
+28 -1
View File
@@ -1,12 +1,13 @@
import pytest
from django.urls import reverse
from apps.accounts.models import User
from apps.accounts.models import TicketMagicLink, User
from apps.core.models import PageTitle, SiteBranding
from apps.customers.models import ContactGroup
from apps.notifications.models import SMSTemplate
from apps.staff.models import OrganizationalUnit
from apps.tickets.models import GroupDispatch, TicketCategory
from apps.tickets.services import create_ticket
@pytest.mark.django_db
@@ -66,6 +67,32 @@ def test_customer_new_renders_developer_credit_once_directly_below_wizard(client
assert '<footer class="site-footer">' not in content
@pytest.mark.django_db
def test_magic_ticket_renders_database_developer_credit_once_inside_workspace(client):
branding = SiteBranding.load()
branding.developer_credit_text = "برنامه نویسی و توسعه: محمد جواد فصاحت | 09202202129"
branding.save()
customer = User.objects.create_user(
username="magic-credit-customer", mobile="09120000414", account_type=User.AccountType.CUSTOMER
)
category = TicketCategory.objects.create(title="پیگیری لینک امضا", slug="magic-credit-category")
ticket = create_ticket(customer=customer, category=category, subject="نمایش امضا", body="شرح")
token = TicketMagicLink.issue(ticket)
destination = client.get(reverse("accounts:magic", args=[token])).url
content = client.get(destination).content.decode()
assert content.count(branding.developer_credit_text) == 1
assert '<footer class="ticket-detail-credit">' in content
assert content.index('class="workspace-grid customer-grid"') < content.index('<footer class="ticket-detail-credit">')
assert '<footer class="site-footer">' not in content
branding.developer_credit_html = '<strong data-developer-signature>امضای HTML سفارشی</strong>'
branding.use_developer_credit_html = True
branding.save()
html_content = client.get(destination).content.decode()
assert '<strong data-developer-signature>امضای HTML سفارشی</strong>' in html_content
@pytest.mark.django_db
def test_staff_sidebar_brand_is_database_backed(client):
user = User.objects.create_superuser(username="brand-admin", password="safe-password", account_type=User.AccountType.STAFF)
+64
View File
@@ -405,6 +405,70 @@ def test_staff_can_visually_add_and_remove_contact_group_memberships(client):
assert set(contact.customer_profile.groups.values_list("pk", flat=True)) == {second.pk}
@pytest.mark.django_db
def test_staff_can_classify_and_edit_ticket_customer_without_an_existing_group(client):
unit = OrganizationalUnit.objects.create(name="واحد مشتری خودکار", slug="ungrouped-customer-editor")
unit.permissions.add(*Permission.objects.filter(
content_type__app_label="customers",
codename__in=("view_customerprofile", "change_customerprofile"),
))
staff = make_staff("ungrouped-customer-agent", unit)
contact = make_contact("ungrouped-ticket-customer", "09120000515")
category = TicketCategory.objects.create(unit=unit, title="درخواست مشتری خودکار", slug="ungrouped-customer-ticket")
create_ticket(customer=contact, created_by=staff, unit=unit, category=category, subject="نیازمند دسته‌بندی", body="شرح")
group = ContactGroup.objects.create(name="گروه انتخاب‌شده", unit=unit)
client.force_login(staff)
url = reverse("staff_tickets:contact_edit", args=[contact.public_id])
page = client.get(url)
assert page.status_code == 200
assert f'value="{group.pk}"' in page.content.decode()
missing_group = client.post(url, {
"mobile": contact.local_mobile,
"first_name": "مشتری",
"last_name": "خودکار",
"organization_name": "",
"stakeholder_type": CustomerProfile.StakeholderType.CUSTOMER,
"groups": [],
"province": "",
"city": "",
})
assert missing_group.status_code == 200
assert missing_group.context["form"]["groups"].errors
saved = client.post(url, {
"mobile": contact.local_mobile,
"first_name": "مشتری",
"last_name": "دسته‌بندی‌شده",
"organization_name": "",
"stakeholder_type": CustomerProfile.StakeholderType.CUSTOMER,
"groups": [group.pk],
"province": "",
"city": "",
})
assert saved.status_code == 302
assert contact.customer_profile.groups.filter(pk=group.pk).exists()
@pytest.mark.django_db
def test_staff_cannot_edit_ungrouped_customer_from_an_unrelated_ticket_unit(client):
ticket_unit = OrganizationalUnit.objects.create(name="واحد مالک مشتری", slug="ungrouped-owner-unit")
unrelated_unit = OrganizationalUnit.objects.create(name="واحد نامرتبط مشتری", slug="ungrouped-unrelated-unit")
unrelated_unit.permissions.add(*Permission.objects.filter(
content_type__app_label="customers",
codename__in=("view_customerprofile", "change_customerprofile"),
))
owner = make_staff("ungrouped-owner-agent", ticket_unit)
outsider = make_staff("ungrouped-outsider-agent", unrelated_unit)
contact = make_contact("private-ungrouped-customer", "09120000516")
category = TicketCategory.objects.create(unit=ticket_unit, title="درخواست محرمانه", slug="private-ungrouped-ticket")
create_ticket(customer=contact, created_by=owner, unit=ticket_unit, category=category, subject="محرمانه", body="شرح")
client.force_login(outsider)
assert client.get(reverse("staff_tickets:contact_edit", args=[contact.public_id])).status_code == 404
@pytest.mark.django_db
def test_staff_ticket_create_supports_multiple_previewable_attachments(client, monkeypatch, settings, tmp_path):
settings.MEDIA_ROOT = tmp_path