some improve for sms delivery
This commit is contained in:
1 parent
1d3cff0b63
commit
ffc1ec0dcf
10 files changed
+134
-11
No files matched your search
@@ -28,7 +28,7 @@ def customer_reply_bale_policy():
|
|||||||
return NotificationPolicy.objects.get_or_create(key=NotificationPolicy.Key.CUSTOMER_REPLY_BALE,defaults={"quiet_period_seconds":600})[0]
|
return NotificationPolicy.objects.get_or_create(key=NotificationPolicy.Key.CUSTOMER_REPLY_BALE,defaults={"quiet_period_seconds":600})[0]
|
||||||
|
|
||||||
def cancel_staff_reply_sms(ticket,reason):
|
def cancel_staff_reply_sms(ticket,reason):
|
||||||
return NotificationJob.objects.filter(ticket=ticket,channel=NotificationJob.Channel.SMS,event_type="STAFF_REPLY",status__in=[NotificationJob.Status.PENDING,NotificationJob.Status.FAILED]).update(status=NotificationJob.Status.CANCELLED,last_error=reason)
|
return NotificationJob.objects.filter(ticket=ticket,channel=NotificationJob.Channel.SMS,event_type="STAFF_REPLY",status__in=[NotificationJob.Status.PENDING,NotificationJob.Status.RETRYING,NotificationJob.Status.FAILED]).update(status=NotificationJob.Status.CANCELLED,last_error=reason)
|
||||||
|
|
||||||
def schedule_staff_reply_sms(ticket,message):
|
def schedule_staff_reply_sms(ticket,message):
|
||||||
"""Debounce a chat burst into one SMS; caller already locks the ticket."""
|
"""Debounce a chat burst into one SMS; caller already locks the ticket."""
|
||||||
@@ -46,7 +46,7 @@ def schedule_staff_reply_sms(ticket,message):
|
|||||||
return NotificationJob.objects.create(channel=NotificationJob.Channel.SMS,event_type="STAFF_REPLY",ticket=ticket,customer=ticket.customer,payload=payload,next_attempt_at=due,dedupe_key=f"ticket:{ticket.pk}:staff-reply:{uuid4().hex}"),True
|
return NotificationJob.objects.create(channel=NotificationJob.Channel.SMS,event_type="STAFF_REPLY",ticket=ticket,customer=ticket.customer,payload=payload,next_attempt_at=due,dedupe_key=f"ticket:{ticket.pk}:staff-reply:{uuid4().hex}"),True
|
||||||
|
|
||||||
def cancel_staff_reply_sms_if_read(ticket,customer):
|
def cancel_staff_reply_sms_if_read(ticket,customer):
|
||||||
jobs=NotificationJob.objects.filter(ticket=ticket,customer=customer,channel=NotificationJob.Channel.SMS,event_type="STAFF_REPLY",status__in=[NotificationJob.Status.PENDING,NotificationJob.Status.FAILED])
|
jobs=NotificationJob.objects.filter(ticket=ticket,customer=customer,channel=NotificationJob.Channel.SMS,event_type="STAFF_REPLY",status__in=[NotificationJob.Status.PENDING,NotificationJob.Status.RETRYING,NotificationJob.Status.FAILED])
|
||||||
cancelled=0
|
cancelled=0
|
||||||
for job in jobs:
|
for job in jobs:
|
||||||
message_id=job.payload.get("latest_message_id")
|
message_id=job.payload.get("latest_message_id")
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
/*! 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
|
||||||
@@ -36,7 +36,7 @@ document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{
|
|||||||
const replyResult=workspace.dataset.replyResult||new URLSearchParams(location.search).get('reply_result')||'';
|
const replyResult=workspace.dataset.replyResult||new URLSearchParams(location.search).get('reply_result')||'';
|
||||||
const submissionSucceeded=replyResult==='reply'||replyResult==='note';
|
const submissionSucceeded=replyResult==='reply'||replyResult==='note';
|
||||||
if(submissionSucceeded){sessionStorage.removeItem(`${baseKey}-reply`);sessionStorage.removeItem(`${baseKey}-note`)}
|
if(submissionSucceeded){sessionStorage.removeItem(`${baseKey}-reply`);sessionStorage.removeItem(`${baseKey}-note`)}
|
||||||
const setComposerOpen=(open,focus=true)=>{composer?.classList.toggle('is-collapsed',!open);composerToggle?.setAttribute('aria-expanded',String(open));if(open&&focus)setTimeout(()=>input?.focus(),80)};
|
const setComposerOpen=(open,focus=true)=>{const keepLatestVisible=atBottom();composer?.classList.toggle('is-collapsed',!open);composerToggle?.setAttribute('aria-expanded',String(open));requestAnimationFrame(()=>{if(keepLatestVisible)scrollLatest(false)});if(open&&focus)setTimeout(()=>input?.focus(),80)};
|
||||||
const saveDraft=()=>{if(!input)return;sessionStorage.setItem(draftKey(),input.value);if(status){status.textContent=input.value?'پیشنویس ذخیره شد':'';clearTimeout(saveDraft.timer);saveDraft.timer=setTimeout(()=>status.textContent='',1800)}};
|
const saveDraft=()=>{if(!input)return;sessionStorage.setItem(draftKey(),input.value);if(status){status.textContent=input.value?'پیشنویس ذخیره شد':'';clearTimeout(saveDraft.timer);saveDraft.timer=setTimeout(()=>status.textContent='',1800)}};
|
||||||
const loadDraft=()=>{if(!input)return;input.value=sessionStorage.getItem(draftKey())||'';resize();input.dispatchEvent(new Event('input',{bubbles:true}))};
|
const loadDraft=()=>{if(!input)return;input.value=sessionStorage.getItem(draftKey())||'';resize();input.dispatchEvent(new Event('input',{bubbles:true}))};
|
||||||
if(input){input.setAttribute('rows','5');input.setAttribute('placeholder','شرح کامل پاسخ، اطلاعات تکمیلی و نتیجه مورد انتظار خود را بنویسید…');input.addEventListener('input',()=>{resize();saveDraft()});loadDraft()}
|
if(input){input.setAttribute('rows','5');input.setAttribute('placeholder','شرح کامل پاسخ، اطلاعات تکمیلی و نتیجه مورد انتظار خود را بنویسید…');input.addEventListener('input',()=>{resize();saveDraft()});loadDraft()}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ body:has(.ticket-workspace){overflow:hidden}.customer-main:has(.ticket-workspace
|
|||||||
.attachment-list{@apply mt-3 grid gap-2}.attachment{margin-top:0}.attachment-image{@apply block max-w-72 overflow-hidden p-1}.attachment-image img{@apply max-h-52 w-full rounded-lg object-cover}.attachment-image span{@apply block truncate px-2 py-1 text-[10px]}.attachment-file{@apply flex max-w-sm items-center gap-3 bg-white/75}.file-icon{@apply grid size-9 shrink-0 place-items-center rounded-lg bg-mint text-lg}.attachment-file b,.attachment-file small{@apply block}.attachment-file small{@apply mt-0.5 text-[9px] font-normal text-ink/40}.attachment-audio{@apply block max-w-md}.attachment-audio audio{@apply my-2 h-10 w-full}.attachment-audio>a{@apply text-[10px]}.message-menu{position:absolute;top:.35rem;left:.35rem}.message.mine .message-menu{left:auto;right:.35rem}.message-menu>summary{@apply grid size-8 cursor-pointer list-none place-items-center rounded-full text-xs tracking-widest text-ink/40 transition hover:bg-white hover:text-sea;box-shadow:none}.message-menu>summary::-webkit-details-marker{display:none}.message-menu[open]>summary{@apply bg-white text-sea;box-shadow:0 5px 15px -10px #08376b}.message-menu-panel{@apply absolute left-0 z-20 mt-1 w-72 overflow-hidden rounded-2xl border border-ink/10 bg-paper shadow-xl}.message.mine .message-menu-panel{right:0;left:auto}.message-menu-head{@apply flex items-center justify-between border-b border-ink/8 bg-ink/[.025] px-4 py-3}.message-menu-head b,.message-menu-head small{@apply block}.message-menu-head b{@apply text-xs text-ink}.message-menu-head small{@apply mt-0.5 text-[9px] font-normal text-ink/45}.message-menu-close{@apply grid size-8 min-h-0 place-items-center rounded-full bg-transparent p-0 text-lg font-normal text-ink/45 shadow-none hover:bg-ink/5 hover:text-ink}.message-edit-form{@apply p-4}.message-edit-form label{@apply mb-1.5 text-[10px] text-ink/65}.message-menu-panel textarea{@apply min-h-24 rounded-xl bg-ink/[.025] p-3 text-xs leading-6 shadow-none}.message-menu-actions{@apply mt-3 grid grid-cols-2 gap-2}.message-menu-actions button{@apply m-0 min-h-10 w-full rounded-xl px-3 py-2 text-xs}.message-edit-save{@apply bg-sea}.message-edit-cancel{@apply border border-ink/10 bg-white text-ink/60 shadow-none hover:bg-ink/5 hover:text-ink}.message-delete-zone{@apply flex items-center justify-between gap-3 border-t border-red-200 bg-red-50/50 px-4 py-3}.message-delete-zone b,.message-delete-zone small{@apply block}.message-delete-zone b{@apply text-[10px] text-red-700}.message-delete-zone small{@apply mt-0.5 text-[9px] text-red-700/60}.message-delete-zone form{@apply m-0}.message-delete-button{@apply m-0 min-h-8 rounded-lg border border-red-200 bg-white px-3 py-1 text-[10px] text-red-700 shadow-none hover:bg-red-500 hover:text-white}@media(max-width:639px){.message-menu-panel{position:fixed;right:.75rem!important;bottom:5rem;left:.75rem!important;top:auto;width:auto;max-height:calc(100dvh - 7rem);overflow:auto}}.conversation-empty{@apply grid h-full min-h-64 place-content-center text-center}.conversation-empty>span{@apply mx-auto mb-3 grid size-12 place-items-center rounded-full bg-mint text-sea}.conversation-empty p{@apply mt-1 text-xs text-ink/45}
|
.attachment-list{@apply mt-3 grid gap-2}.attachment{margin-top:0}.attachment-image{@apply block max-w-72 overflow-hidden p-1}.attachment-image img{@apply max-h-52 w-full rounded-lg object-cover}.attachment-image span{@apply block truncate px-2 py-1 text-[10px]}.attachment-file{@apply flex max-w-sm items-center gap-3 bg-white/75}.file-icon{@apply grid size-9 shrink-0 place-items-center rounded-lg bg-mint text-lg}.attachment-file b,.attachment-file small{@apply block}.attachment-file small{@apply mt-0.5 text-[9px] font-normal text-ink/40}.attachment-audio{@apply block max-w-md}.attachment-audio audio{@apply my-2 h-10 w-full}.attachment-audio>a{@apply text-[10px]}.message-menu{position:absolute;top:.35rem;left:.35rem}.message.mine .message-menu{left:auto;right:.35rem}.message-menu>summary{@apply grid size-8 cursor-pointer list-none place-items-center rounded-full text-xs tracking-widest text-ink/40 transition hover:bg-white hover:text-sea;box-shadow:none}.message-menu>summary::-webkit-details-marker{display:none}.message-menu[open]>summary{@apply bg-white text-sea;box-shadow:0 5px 15px -10px #08376b}.message-menu-panel{@apply absolute left-0 z-20 mt-1 w-72 overflow-hidden rounded-2xl border border-ink/10 bg-paper shadow-xl}.message.mine .message-menu-panel{right:0;left:auto}.message-menu-head{@apply flex items-center justify-between border-b border-ink/8 bg-ink/[.025] px-4 py-3}.message-menu-head b,.message-menu-head small{@apply block}.message-menu-head b{@apply text-xs text-ink}.message-menu-head small{@apply mt-0.5 text-[9px] font-normal text-ink/45}.message-menu-close{@apply grid size-8 min-h-0 place-items-center rounded-full bg-transparent p-0 text-lg font-normal text-ink/45 shadow-none hover:bg-ink/5 hover:text-ink}.message-edit-form{@apply p-4}.message-edit-form label{@apply mb-1.5 text-[10px] text-ink/65}.message-menu-panel textarea{@apply min-h-24 rounded-xl bg-ink/[.025] p-3 text-xs leading-6 shadow-none}.message-menu-actions{@apply mt-3 grid grid-cols-2 gap-2}.message-menu-actions button{@apply m-0 min-h-10 w-full rounded-xl px-3 py-2 text-xs}.message-edit-save{@apply bg-sea}.message-edit-cancel{@apply border border-ink/10 bg-white text-ink/60 shadow-none hover:bg-ink/5 hover:text-ink}.message-delete-zone{@apply flex items-center justify-between gap-3 border-t border-red-200 bg-red-50/50 px-4 py-3}.message-delete-zone b,.message-delete-zone small{@apply block}.message-delete-zone b{@apply text-[10px] text-red-700}.message-delete-zone small{@apply mt-0.5 text-[9px] text-red-700/60}.message-delete-zone form{@apply m-0}.message-delete-button{@apply m-0 min-h-8 rounded-lg border border-red-200 bg-white px-3 py-1 text-[10px] text-red-700 shadow-none hover:bg-red-500 hover:text-white}@media(max-width:639px){.message-menu-panel{position:fixed;right:.75rem!important;bottom:5rem;left:.75rem!important;top:auto;width:auto;max-height:calc(100dvh - 7rem);overflow:auto}}.conversation-empty{@apply grid h-full min-h-64 place-content-center text-center}.conversation-empty>span{@apply mx-auto mb-3 grid size-12 place-items-center rounded-full bg-mint text-sea}.conversation-empty p{@apply mt-1 text-xs text-ink/45}
|
||||||
.ticket-workspace .composer{position:relative;z-index:15;margin:0;border:0;border-top:1px solid rgba(8,55,107,.08);border-radius:0;padding:.65rem .85rem max(.65rem,env(safe-area-inset-bottom));box-shadow:0 -14px 40px -30px rgba(8,55,107,.55)}.composer-context{@apply mb-2 flex items-start justify-between}.composer-context b,.composer-context small{@apply block}.composer-context b{@apply text-xs}.composer-context small,.composer-context>[data-draft-status]{@apply mt-0.5 text-[9px] text-ink/40}.ticket-workspace .composer textarea{max-height:9rem;min-height:2.75rem;resize:none;border-radius:13px;background:var(--color-cream);padding:.7rem .85rem;line-height:1.7}.ticket-workspace input[type=file]{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}.composer-bar{@apply mt-2 flex items-center gap-2}.composer-tools{@apply flex items-center gap-1}.tool-button{@apply mb-0 inline-flex min-h-10 cursor-pointer items-center gap-1 rounded-xl border border-ink/8 bg-paper px-3 text-xs font-bold text-ink shadow-none hover:border-sea/30 hover:bg-mint}.tool-button span{@apply hidden sm:inline}.composer-hint{@apply hidden flex-1 text-center text-[9px] text-ink/30 lg:block}.send-button{@apply mr-auto min-h-10 rounded-xl bg-sea px-4 py-2}.compact-recorder{@apply my-2 rounded-xl p-3}.compact-recorder[hidden],.composer-extra[hidden]{display:none}.composer-extra{@apply mt-2 rounded-xl border border-ink/8 bg-cream p-3}.composer-extra label{@apply text-xs}.new-message-jump{@apply absolute bottom-36 left-1/2 z-20 min-h-9 -translate-x-1/2 rounded-full bg-ink px-4 py-2 text-[10px] shadow-xl}.new-message-jump[hidden]{display:none}.staff-composer .mode{@apply mb-2}.staff-composer.note-mode{background:var(--theme-note);border-top-color:color-mix(in srgb,var(--color-coral) 45%,transparent)}.staff-composer.note-mode textarea{@apply bg-white}.staff-composer.note-mode .send-button{@apply bg-coral text-ink}.file-preview:empty{display:none}.ticket-workspace .file-preview{@apply mb-2 mt-2 grid grid-cols-2 gap-2 sm:grid-cols-4}.ticket-workspace .file-chip{@apply text-[9px]}
|
.ticket-workspace .composer{position:relative;z-index:15;margin:0;border:0;border-top:1px solid rgba(8,55,107,.08);border-radius:0;padding:.65rem .85rem max(.65rem,env(safe-area-inset-bottom));box-shadow:0 -14px 40px -30px rgba(8,55,107,.55)}.composer-context{@apply mb-2 flex items-start justify-between}.composer-context b,.composer-context small{@apply block}.composer-context b{@apply text-xs}.composer-context small,.composer-context>[data-draft-status]{@apply mt-0.5 text-[9px] text-ink/40}.ticket-workspace .composer textarea{max-height:9rem;min-height:2.75rem;resize:none;border-radius:13px;background:var(--color-cream);padding:.7rem .85rem;line-height:1.7}.ticket-workspace input[type=file]{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}.composer-bar{@apply mt-2 flex items-center gap-2}.composer-tools{@apply flex items-center gap-1}.tool-button{@apply mb-0 inline-flex min-h-10 cursor-pointer items-center gap-1 rounded-xl border border-ink/8 bg-paper px-3 text-xs font-bold text-ink shadow-none hover:border-sea/30 hover:bg-mint}.tool-button span{@apply hidden sm:inline}.composer-hint{@apply hidden flex-1 text-center text-[9px] text-ink/30 lg:block}.send-button{@apply mr-auto min-h-10 rounded-xl bg-sea px-4 py-2}.compact-recorder{@apply my-2 rounded-xl p-3}.compact-recorder[hidden],.composer-extra[hidden]{display:none}.composer-extra{@apply mt-2 rounded-xl border border-ink/8 bg-cream p-3}.composer-extra label{@apply text-xs}.new-message-jump{@apply absolute bottom-36 left-1/2 z-20 min-h-9 -translate-x-1/2 rounded-full bg-ink px-4 py-2 text-[10px] shadow-xl}.new-message-jump[hidden]{display:none}.staff-composer .mode{@apply mb-2}.staff-composer.note-mode{background:var(--theme-note);border-top-color:color-mix(in srgb,var(--color-coral) 45%,transparent)}.staff-composer.note-mode textarea{@apply bg-white}.staff-composer.note-mode .send-button{@apply bg-coral text-ink}.file-preview:empty{display:none}.ticket-workspace .file-preview{@apply mb-2 mt-2 grid grid-cols-2 gap-2 sm:grid-cols-4}.ticket-workspace .file-chip{@apply text-[9px]}
|
||||||
.workspace-side{min-height:0;overflow-y:auto;padding:.75rem;background:var(--color-cream)}.workspace-side .info-card{@apply mb-3 rounded-[14px] p-4 shadow-none}.panel-close{@apply hidden}.ticket-operations form{@apply mt-3 border-t border-ink/6 pt-3}.ticket-operations form button{@apply w-full}.ticket-operations details{@apply mt-3 rounded-xl border border-ink/8 p-3}.ticket-operations summary{@apply cursor-pointer text-xs font-bold}
|
.workspace-side{min-height:0;overflow-y:auto;padding:.75rem;background:var(--color-cream)}.workspace-side .info-card{@apply mb-3 rounded-[14px] p-4 shadow-none}.panel-close{@apply hidden}.ticket-operations form{@apply mt-3 border-t border-ink/6 pt-3}.ticket-operations form button{@apply w-full}.ticket-operations details{@apply mt-3 rounded-xl border border-ink/8 p-3}.ticket-operations summary{@apply cursor-pointer text-xs font-bold}
|
||||||
|
.sms-delivery-heading{display:flex;align-items:center;justify-content:space-between;gap:.75rem}.sms-delivery-heading h3,.sms-delivery-heading small{display:block}.sms-delivery-heading small{margin-top:.2rem;color:rgba(8,55,107,.48);font-size:.6rem}.sms-delivery-heading>span{display:grid;width:1.75rem;height:1.75rem;flex:none;place-items:center;border-radius:999px;background:var(--color-mint);color:var(--color-sea);font-weight:900}.sms-delivery-list{display:grid;gap:.45rem;margin-top:.75rem}.sms-delivery-item{border:1px solid rgba(8,55,107,.09);border-radius:.75rem;background:var(--color-paper);overflow:hidden}.sms-delivery-item>summary{display:grid;grid-template-columns:minmax(0,1fr) auto 1.6rem;align-items:center;gap:.45rem;min-height:2.8rem;padding:.5rem .6rem;cursor:pointer;list-style:none}.sms-delivery-item>summary::-webkit-details-marker{display:none}.sms-delivery-event{min-width:0;overflow:hidden;color:rgba(8,55,107,.68);font-size:.65rem;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.sms-delivery-item>summary b{font-size:.64rem;color:var(--color-ink)}.sms-delivery-info{display:grid;width:1.55rem;height:1.55rem;place-items:center;border:1px solid rgba(6,147,227,.25);border-radius:999px;background:#eef8ff;color:var(--color-sea);font-size:.68rem;font-weight:900}.sms-delivery-item[open] .sms-delivery-info{background:var(--color-sea);color:#fff}.sms-delivery-timeline{display:grid;gap:.7rem;margin:0;padding:.7rem .75rem .75rem;list-style:none;border-top:1px solid rgba(8,55,107,.07);background:var(--color-cream)}.sms-delivery-timeline li{display:grid;grid-template-columns:.65rem minmax(0,1fr);gap:.55rem;align-items:start;color:rgba(8,55,107,.48)}.sms-delivery-timeline li>span{width:.55rem;height:.55rem;margin-top:.28rem;border:2px solid #aab9c6;border-radius:999px;background:#fff}.sms-delivery-timeline li.is-complete>span{border-color:var(--color-sea);background:var(--color-sea)}.sms-delivery-timeline b,.sms-delivery-timeline time,.sms-delivery-timeline small{display:block}.sms-delivery-timeline b{font-size:.62rem}.sms-delivery-timeline time,.sms-delivery-timeline small{margin-top:.12rem;font-size:.58rem;color:rgba(8,55,107,.58)}.sms-delivery-timeline li.is-complete b{color:var(--color-ink)}.sms-delivery-description,.sms-delivery-error,.sms-delivery-tracking{display:block;margin:0;padding:.55rem .75rem;border-top:1px solid rgba(8,55,107,.07);font-size:.58rem;line-height:1.75}.sms-delivery-description{color:#526b85}.sms-delivery-error{color:#8e2d2d;background:#fff2f2}.sms-delivery-tracking{overflow-wrap:anywhere;color:#647b91;text-align:left}
|
||||||
@media(max-width:1023px){body:has(.ticket-workspace) .mobile-dock,body:has(.ticket-workspace) .staff-mobile-dock{display:none}.ticket-workspace{height:calc(100dvh - 4.25rem)}.staff-conversation-grid,.customer-grid{grid-template-columns:minmax(0,1fr)}.workspace-side{position:fixed;inset:4.25rem 0 0 auto;z-index:60;width:min(88vw,22rem);transform:translateX(105%);border-left:1px solid rgba(8,55,107,.1);box-shadow:-20px 0 60px -35px rgba(8,55,107,.7);transition:transform .25s ease}.workspace-side.panel-open{transform:translateX(0)}.panel-close{@apply mb-3 grid size-10 place-items-center rounded-xl bg-ink text-white}.message-bubble{max-width:86%}}
|
@media(max-width:1023px){body:has(.ticket-workspace) .mobile-dock,body:has(.ticket-workspace) .staff-mobile-dock{display:none}.ticket-workspace{height:calc(100dvh - 4.25rem)}.staff-conversation-grid,.customer-grid{grid-template-columns:minmax(0,1fr)}.workspace-side{position:fixed;inset:4.25rem 0 0 auto;z-index:60;width:min(88vw,22rem);transform:translateX(105%);border-left:1px solid rgba(8,55,107,.1);box-shadow:-20px 0 60px -35px rgba(8,55,107,.7);transition:transform .25s ease}.workspace-side.panel-open{transform:translateX(0)}.panel-close{@apply mb-3 grid size-10 place-items-center rounded-xl bg-ink text-white}.message-bubble{max-width:86%}}
|
||||||
@media(max-width:639px){.site-header{display:none}.customer-main:has(.ticket-workspace){padding:0}.ticket-workspace{height:100dvh}.ticket-workspace-head{@apply min-h-16}.workspace-grid{height:calc(100% - 4rem)}.ticket-title h1{@apply max-w-[48vw] text-sm}.ticket-head-actions .badge{@apply max-w-24 truncate}.conversation-toolbar{@apply min-h-10}.ticket-workspace .conversation{@apply px-2.5 py-3}.message-avatar{@apply size-7 rounded-lg}.message-bubble{max-width:88%;padding:.6rem .7rem}.message.note .message-bubble{width:96%}.ticket-workspace .composer{@apply px-2.5 pt-2}.composer-context small{max-width:70vw}.composer-hint{display:none}.send-button{@apply px-3}.new-message-jump{bottom:8.5rem}.ticket-head-actions form{display:none}}
|
@media(max-width:639px){.site-header{display:none}.customer-main:has(.ticket-workspace){padding:0}.ticket-workspace{height:100dvh}.ticket-workspace-head{@apply min-h-16}.workspace-grid{height:calc(100% - 4rem)}.ticket-title h1{@apply max-w-[48vw] text-sm}.ticket-head-actions .badge{@apply max-w-24 truncate}.conversation-toolbar{@apply min-h-10}.ticket-workspace .conversation{@apply px-2.5 py-3}.message-avatar{@apply size-7 rounded-lg}.message-bubble{max-width:88%;padding:.6rem .7rem}.message.note .message-bubble{width:96%}.ticket-workspace .composer{@apply px-2.5 pt-2}.composer-context small{max-width:70vw}.composer-hint{display:none}.send-button{@apply px-3}.new-message-jump{bottom:8.5rem}.ticket-head-actions form{display:none}}
|
||||||
@media(prefers-reduced-motion:reduce){.ticket-workspace .conversation{scroll-behavior:auto}.workspace-side{transition:none}}
|
@media(prefers-reduced-motion:reduce){.ticket-workspace .conversation{scroll-behavior:auto}.workspace-side{transition:none}}
|
||||||
@@ -41,13 +42,13 @@ body:has(.ticket-workspace){overflow:hidden}.customer-main:has(.ticket-workspace
|
|||||||
|
|
||||||
/* Collapsible reply panel keeps the correspondence history dominant. */
|
/* Collapsible reply panel keeps the correspondence history dominant. */
|
||||||
.ticket-workspace .composer.is-collapsed{padding:.55rem .85rem;border-top:1px solid rgba(8,55,107,.1);background:var(--color-paper)}.composer.is-collapsed>form,.composer.is-collapsed>.mode{display:none}.composer-launch{display:none;width:100%;min-height:3.5rem;align-items:center;justify-content:flex-start;gap:.75rem;border:1px solid rgba(8,55,107,.1);border-radius:11px;background:linear-gradient(135deg,var(--color-paper),var(--color-mint));padding:.55rem .75rem;color:var(--color-ink);box-shadow:none;text-align:right}.composer.is-collapsed .composer-launch{display:flex}.composer-launch:hover{transform:none;border-color:color-mix(in srgb,var(--color-sea) 35%,transparent);background:var(--color-mint)}.composer-launch-icon{@apply grid size-9 shrink-0 place-items-center rounded-lg bg-sea text-lg text-white}.composer-launch b,.composer-launch small{display:block}.composer-launch b{@apply text-xs}.composer-launch small{@apply mt-0.5 text-[9px] font-medium text-ink/45}.composer-launch-arrow{@apply mr-auto text-sm text-sea}.composer-close{@apply text-ink/55}.composer:not(.is-collapsed) .composer-launch{display:none}.composer.is-collapsed~*{transition:none}.composer.is-collapsed+.new-message-jump{bottom:5rem}.ticket-workspace .composer.is-collapsed{box-shadow:0 -10px 30px -28px rgba(8,55,107,.65)}
|
.ticket-workspace .composer.is-collapsed{padding:.55rem .85rem;border-top:1px solid rgba(8,55,107,.1);background:var(--color-paper)}.composer.is-collapsed>form,.composer.is-collapsed>.mode{display:none}.composer-launch{display:none;width:100%;min-height:3.5rem;align-items:center;justify-content:flex-start;gap:.75rem;border:1px solid rgba(8,55,107,.1);border-radius:11px;background:linear-gradient(135deg,var(--color-paper),var(--color-mint));padding:.55rem .75rem;color:var(--color-ink);box-shadow:none;text-align:right}.composer.is-collapsed .composer-launch{display:flex}.composer-launch:hover{transform:none;border-color:color-mix(in srgb,var(--color-sea) 35%,transparent);background:var(--color-mint)}.composer-launch-icon{@apply grid size-9 shrink-0 place-items-center rounded-lg bg-sea text-lg text-white}.composer-launch b,.composer-launch small{display:block}.composer-launch b{@apply text-xs}.composer-launch small{@apply mt-0.5 text-[9px] font-medium text-ink/45}.composer-launch-arrow{@apply mr-auto text-sm text-sea}.composer-close{@apply text-ink/55}.composer:not(.is-collapsed) .composer-launch{display:none}.composer.is-collapsed~*{transition:none}.composer.is-collapsed+.new-message-jump{bottom:5rem}.ticket-workspace .composer.is-collapsed{box-shadow:0 -10px 30px -28px rgba(8,55,107,.65)}
|
||||||
@media(max-width:639px){.ticket-workspace .composer.is-collapsed{@apply px-2.5 py-2}.composer-launch{min-height:3.25rem}.composer-launch small{max-width:65vw}.new-message-jump{bottom:5.5rem}.staff-ticket-workspace .composer:not(.is-collapsed){position:fixed;inset-inline:0;bottom:0;z-index:55;box-sizing:border-box;max-height:calc(100dvh - .5rem);overflow-y:auto;overscroll-behavior:contain;border-radius:1rem 1rem 0 0;padding-bottom:max(1rem,env(safe-area-inset-bottom));box-shadow:0 -18px 55px -25px rgba(8,55,107,.75)}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-bar{position:sticky;bottom:calc(-1 * max(1rem,env(safe-area-inset-bottom)));z-index:2;margin-inline:-.65rem;margin-bottom:calc(-1 * max(1rem,env(safe-area-inset-bottom)));padding:.75rem .65rem max(1rem,env(safe-area-inset-bottom));background:var(--color-paper)}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-tools{display:grid;grid-template-columns:1fr 1fr 1fr;width:100%}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-tools .tool-button{justify-content:center;min-width:0;padding-inline:.4rem}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-close{display:inline-flex;order:initial;color:var(--color-ink)} }
|
@media(max-width:639px){.ticket-workspace .composer.is-collapsed{@apply px-2.5 py-2}.composer-launch{min-height:3.25rem}.composer-launch small{max-width:65vw}.new-message-jump{bottom:5.5rem}.ticket-workspace .composer{min-width:0;max-height:min(45dvh,28rem);overflow-y:auto;overscroll-behavior:contain}.ticket-workspace .composer-bar{position:sticky;bottom:calc(-1 * max(1rem,env(safe-area-inset-bottom)));z-index:2;margin-inline:-.65rem;margin-bottom:calc(-1 * max(1rem,env(safe-area-inset-bottom)));padding:.75rem .65rem max(1rem,env(safe-area-inset-bottom));background:var(--color-paper)}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-tools{display:grid;grid-template-columns:1fr 1fr 1fr;width:100%}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-tools .tool-button{justify-content:center;min-width:0;padding-inline:.4rem}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-close{display:inline-flex;order:initial;color:var(--color-ink)} }
|
||||||
.conversation-pane:has(.composer.is-collapsed) .new-message-jump{bottom:5rem}.conversation-pane:not(:has(.composer.is-collapsed)) .new-message-jump{bottom:17rem}@media(max-width:639px){.conversation-pane:has(.composer.is-collapsed) .new-message-jump{bottom:5.5rem}.conversation-pane:not(:has(.composer.is-collapsed)) .new-message-jump{bottom:19rem}}
|
.conversation-pane:has(.composer.is-collapsed) .new-message-jump{bottom:5rem}.conversation-pane:not(:has(.composer.is-collapsed)) .new-message-jump{bottom:17rem}@media(max-width:639px){.conversation-pane:has(.composer.is-collapsed) .new-message-jump{bottom:5.5rem}.conversation-pane:not(:has(.composer.is-collapsed)) .new-message-jump{bottom:19rem}}
|
||||||
body:has(.ticket-workspace) .flash{position:fixed;top:5rem;left:50%;z-index:100;width:min(calc(100% - 2rem),42rem);margin:0;transform:translateX(-50%);box-shadow:0 18px 50px -25px rgba(8,55,107,.55);animation:flash-in .22s ease-out}.flash{position:relative;padding-left:3rem}.flash-close{position:absolute;left:.5rem;top:50%;min-height:2rem;width:2rem;transform:translateY(-50%);border-radius:.5rem;background:transparent;padding:0;color:currentColor;box-shadow:none}.flash-close:hover{transform:translateY(-50%);background:rgba(255,255,255,.6)}.flash-leaving{opacity:0;transform:translate(-50%,-.5rem)!important;transition:.25s ease}@keyframes flash-in{from{opacity:0;transform:translate(-50%,-.5rem)}to{opacity:1;transform:translate(-50%,0)}}
|
body:has(.ticket-workspace) .flash{position:fixed;top:5rem;left:50%;z-index:100;width:min(calc(100% - 2rem),42rem);margin:0;transform:translateX(-50%);box-shadow:0 18px 50px -25px rgba(8,55,107,.55);animation:flash-in .22s ease-out}.flash{position:relative;padding-left:3rem}.flash-close{position:absolute;left:.5rem;top:50%;min-height:2rem;width:2rem;transform:translateY(-50%);border-radius:.5rem;background:transparent;padding:0;color:currentColor;box-shadow:none}.flash-close:hover{transform:translateY(-50%);background:rgba(255,255,255,.6)}.flash-leaving{opacity:0;transform:translate(-50%,-.5rem)!important;transition:.25s ease}@keyframes flash-in{from{opacity:0;transform:translate(-50%,-.5rem)}to{opacity:1;transform:translate(-50%,0)}}
|
||||||
|
|
||||||
/* Group context adds a row above the conversation; keep every control inside the viewport. */
|
/* Group context adds a row above the conversation; keep every control inside the viewport. */
|
||||||
.staff-ticket-workspace:has(.ticket-dispatch-context){display:grid;grid-template-rows:auto auto minmax(0,1fr)}.staff-ticket-workspace:has(.ticket-dispatch-context) .workspace-grid{height:auto}.ticket-dispatch-context{margin:.5rem .65rem 0}
|
.staff-ticket-workspace:has(.ticket-dispatch-context){display:grid;grid-template-rows:auto auto minmax(0,1fr)}.staff-ticket-workspace:has(.ticket-dispatch-context) .workspace-grid{height:auto}.ticket-dispatch-context{margin:.5rem .65rem 0}
|
||||||
@media(max-width:639px){.staff-ticket-workspace{min-width:0}.ticket-workspace-head{min-width:0}.ticket-title{flex:1}.ticket-head-actions{gap:.35rem}.ticket-head-actions .icon-button{width:2.35rem;height:2.35rem}.ticket-dispatch-context{margin:.4rem .45rem 0}.composer-launch{min-width:0;padding-inline:.55rem;gap:.5rem}.composer-launch>span:nth-child(2){min-width:0;flex:1}.composer-launch b,.composer-launch small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:none}.composer-launch-arrow{flex:none;margin-right:0}.staff-ticket-workspace .composer.is-collapsed{position:fixed;right:0;bottom:0;left:0;z-index:45;padding-bottom:max(.5rem,env(safe-area-inset-bottom))}.staff-ticket-workspace .conversation{padding-bottom:5.25rem}}
|
@media(max-width:639px){.staff-ticket-workspace{min-width:0}.ticket-workspace-head{min-width:0}.ticket-title{flex:1}.ticket-head-actions{gap:.35rem}.ticket-head-actions .icon-button{width:2.35rem;height:2.35rem}.ticket-dispatch-context{margin:.4rem .45rem 0}.composer-launch{min-width:0;padding-inline:.55rem;gap:.5rem}.composer-launch>span:nth-child(2){min-width:0;flex:1}.composer-launch b,.composer-launch small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:none}.composer-launch-arrow{flex:none;margin-right:0}}
|
||||||
.flash.flash-leaving{transform:none!important}body:has(.ticket-workspace) .flash.flash-leaving{transform:translate(-50%,-.5rem)!important}
|
.flash.flash-leaving{transform:none!important}body:has(.ticket-workspace) .flash.flash-leaving{transform:translate(-50%,-.5rem)!important}
|
||||||
|
|
||||||
/* Searchable Iran province/city fields shared by profile and new-ticket forms. */
|
/* Searchable Iran province/city fields shared by profile and new-ticket forms. */
|
||||||
@@ -90,8 +91,8 @@ body:has(.ticket-workspace) .flash{position:fixed;top:5rem;left:50%;z-index:100;
|
|||||||
.ticket-company-logo{display:block;width:3.25rem;height:auto;max-height:1.45rem;flex-shrink:0;object-fit:contain}
|
.ticket-company-logo{display:block;width:3.25rem;height:auto;max-height:1.45rem;flex-shrink:0;object-fit:contain}
|
||||||
.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}
|
.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)}
|
.staff-main>.site-footer{border-top:1px solid rgba(8,55,107,.06)}
|
||||||
body:has(.ticket-workspace) .ticket-workspace{height:calc(100dvh - 4.25rem - 2rem)}
|
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)}
|
||||||
@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:calc(100dvh - 2rem)}}
|
@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 */
|
/* Session actions, staff-created tickets and install prompt */
|
||||||
.desktop-nav form,.staff-topbar-actions form{margin:0}.logout-button{min-height:auto;border:0;background:transparent;padding:.5rem .75rem;color:var(--color-coral);box-shadow:none}.logout-button:hover{transform:none;background:#fff;color:#b45309;box-shadow:none}
|
.desktop-nav form,.staff-topbar-actions form{margin:0}.logout-button{min-height:auto;border:0;background:transparent;padding:.5rem .75rem;color:var(--color-coral);box-shadow:none}.logout-button:hover{transform:none;background:#fff;color:#b45309;box-shadow:none}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<aside class="workspace-side actions-side" id="ticket-actions" data-workspace-panel>
|
<aside class="workspace-side actions-side" id="ticket-actions" data-workspace-panel>
|
||||||
<button type="button" class="panel-close" data-close-panel>×</button>
|
<button type="button" class="panel-close" data-close-panel>×</button>
|
||||||
<section class="info-card"><p class="panel-index">وضعیت تیکت</p><div class="info-list"><div class="info-row"><span>وضعیت</span><b>{{ ticket.get_status_display }}</b></div><div class="info-row"><span>اولویت</span><b>{{ ticket.get_priority_display }}</b></div><div class="info-row"><span>دسته</span><b>{{ ticket.category }}</b></div><div class="info-row"><span>مالک</span><b>{{ ticket.assigned_expert_name }}</b></div>{% if ticket.first_response_due_at %}<div class="info-row"><span>مهلت پاسخ</span><b>{{ ticket.first_response_due_at|jalali_datetime }}</b></div>{% endif %}</div></section>
|
<section class="info-card"><p class="panel-index">وضعیت تیکت</p><div class="info-list"><div class="info-row"><span>وضعیت</span><b>{{ ticket.get_status_display }}</b></div><div class="info-row"><span>اولویت</span><b>{{ ticket.get_priority_display }}</b></div><div class="info-row"><span>دسته</span><b>{{ ticket.category }}</b></div><div class="info-row"><span>مالک</span><b>{{ ticket.assigned_expert_name }}</b></div>{% if ticket.first_response_due_at %}<div class="info-row"><span>مهلت پاسخ</span><b>{{ ticket.first_response_due_at|jalali_datetime }}</b></div>{% endif %}</div></section>
|
||||||
{% if sms_jobs %}<section class="info-card"><h3>وضعیت پیامکهای مشتری</h3><div class="info-list">{% for sms_job in sms_jobs %}<div class="info-row"><span>{% if sms_job.event_type == 'STAFF_TICKET_CREATED' %}ثبت تیکت توسط کارشناس{% elif sms_job.event_type == 'CUSTOMER_TICKET_CREATED' %}ثبت تیکت مشتری{% elif sms_job.event_type == 'STAFF_REPLY' %}اعلان پاسخ کارشناس{% else %}پیامک سیستمی{% endif %}</span><b>{% if sms_job.delivery_status %}{{ sms_job.get_delivery_status_display }}{% else %}{{ sms_job.get_status_display }}{% endif %}</b></div>{% if sms_job.delivery_description %}<p class="muted">{{ sms_job.delivery_description }}</p>{% endif %}{% if sms_job.last_error %}<div class="create-form-errors">علت دقیق: {{ sms_job.last_error }}</div>{% endif %}{% endfor %}</div></section>{% endif %}
|
{% if sms_jobs %}<section class="info-card sms-delivery-card"><div class="sms-delivery-heading"><div><h3>وضعیت پیامکهای مشتری</h3><small>خلاصه ارسال و تحویل</small></div><span aria-hidden="true">ⓘ</span></div><div class="sms-delivery-list">{% for sms_job in sms_jobs %}<details class="sms-delivery-item"><summary><span class="sms-delivery-event">{% if sms_job.event_type == 'STAFF_TICKET_CREATED' %}ثبت تیکت توسط کارشناس{% elif sms_job.event_type == 'CUSTOMER_TICKET_CREATED' %}ثبت تیکت مشتری{% elif sms_job.event_type == 'STAFF_REPLY' %}اعلان پاسخ کارشناس{% else %}پیامک سیستمی{% endif %}</span><b>{% if sms_job.delivery_status %}{{ sms_job.get_delivery_status_display }}{% else %}{{ sms_job.get_status_display }}{% endif %}</b><span class="sms-delivery-info" aria-label="نمایش جزئیات زمانبندی پیامک" title="نمایش جزئیات">i</span></summary><ol class="sms-delivery-timeline"><li class="is-complete"><span></span><div><b>ثبت درخواست ارسال</b><time datetime="{{ sms_job.created_at|date:'c' }}">{{ sms_job.created_at|jalali_datetime }}</time></div></li><li class="{% if sms_job.sent_at %}is-complete{% endif %}"><span></span><div><b>پذیرش توسط درگاه پیامک</b>{% if sms_job.sent_at %}<time datetime="{{ sms_job.sent_at|date:'c' }}">{{ sms_job.sent_at|jalali_datetime }}</time>{% else %}<small>{% if sms_job.status == 'FAILED' %}ناموفق{% elif sms_job.status == 'CANCELLED' %}لغوشده پیش از ارسال{% else %}در انتظار ارسال{% endif %}</small>{% endif %}</div></li><li class="{% if sms_job.delivery_status == 'DELIVERED' %}is-complete{% endif %}"><span></span><div><b>تأیید رسیدن به گوشی</b>{% if sms_job.delivery_status == 'DELIVERED' and sms_job.delivery_checked_at %}<time datetime="{{ sms_job.delivery_checked_at|date:'c' }}">{{ sms_job.delivery_checked_at|jalali_datetime }}</time>{% elif sms_job.delivery_checked_at %}<small>آخرین استعلام: {{ sms_job.delivery_checked_at|jalali_datetime }}</small>{% else %}<small>{% if sms_job.sent_at %}در انتظار نتیجه اپراتور{% else %}هنوز ارسال نشده{% endif %}</small>{% endif %}</div></li></ol>{% if sms_job.delivery_description %}<p class="sms-delivery-description">{{ sms_job.delivery_description }}</p>{% endif %}{% if sms_job.last_error %}<p class="sms-delivery-error">علت دقیق: {{ sms_job.last_error }}</p>{% endif %}{% if sms_job.provider_message_id %}<small class="sms-delivery-tracking" dir="ltr">شناسه رهگیری: {{ sms_job.provider_message_id }}</small>{% endif %}</details>{% endfor %}</div></section>{% endif %}
|
||||||
<section class="info-card ticket-operations"><h3>عملیات تیکت</h3>{% if perms.tickets.change_ticket %}<form method="post" action="{% url 'staff_tickets:status' ticket.public_id %}">{% csrf_token %}<label>وضعیت<select name="status">{% for value,label in statuses %}<option value="{{ value }}" {% if ticket.status == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><button>ثبت وضعیت</button></form>{% endif %}{% if perms.tickets.change_ticket_priority %}<form method="post" action="{% url 'staff_tickets:priority' ticket.public_id %}">{% csrf_token %}<label>اولویت<select name="priority">{% for value,label in priorities %}<option value="{{ value }}" {% if ticket.priority == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><button class="secondary">ثبت اولویت</button></form>{% endif %}{% if can_transfer %}<details><summary>انتقال تیکت</summary><form method="post" action="{% url 'staff_tickets:transfer' ticket.public_id %}">{% csrf_token %}{{ transfer_form.as_p }}<button>انتقال</button></form></details>{% endif %}{% if can_share %}<details><summary>اشتراک با کارشناس</summary><form method="post" action="{% url 'staff_tickets:share' ticket.public_id %}">{% csrf_token %}{{ share_form.as_p }}<button>ایجاد دسترسی</button></form>{% for grant in grants %}<div class="grant"><span>{{ grant.user.staff_profile.display_name }}</span><form method="post" action="{% url 'staff_tickets:revoke' ticket.public_id grant.user.public_id %}">{% csrf_token %}<button class="danger-button">لغو</button></form></div>{% endfor %}</details>{% endif %}</section>
|
<section class="info-card ticket-operations"><h3>عملیات تیکت</h3>{% if perms.tickets.change_ticket %}<form method="post" action="{% url 'staff_tickets:status' ticket.public_id %}">{% csrf_token %}<label>وضعیت<select name="status">{% for value,label in statuses %}<option value="{{ value }}" {% if ticket.status == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><button>ثبت وضعیت</button></form>{% endif %}{% if perms.tickets.change_ticket_priority %}<form method="post" action="{% url 'staff_tickets:priority' ticket.public_id %}">{% csrf_token %}<label>اولویت<select name="priority">{% for value,label in priorities %}<option value="{{ value }}" {% if ticket.priority == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><button class="secondary">ثبت اولویت</button></form>{% endif %}{% if can_transfer %}<details><summary>انتقال تیکت</summary><form method="post" action="{% url 'staff_tickets:transfer' ticket.public_id %}">{% csrf_token %}{{ transfer_form.as_p }}<button>انتقال</button></form></details>{% endif %}{% if can_share %}<details><summary>اشتراک با کارشناس</summary><form method="post" action="{% url 'staff_tickets:share' ticket.public_id %}">{% csrf_token %}{{ share_form.as_p }}<button>ایجاد دسترسی</button></form>{% for grant in grants %}<div class="grant"><span>{{ grant.user.staff_profile.display_name }}</span><form method="post" action="{% url 'staff_tickets:revoke' ticket.public_id grant.user.public_id %}">{% csrf_token %}<button class="danger-button">لغو</button></form></div>{% endfor %}</details>{% endif %}</section>
|
||||||
{% if can_delete %}<section class="info-card danger-zone"><h3>حذف کامل تیکت</h3><p class="muted">تمام پیامها، فایلها و سوابق تیکت برای همیشه حذف میشوند.</p><form method="post" action="{% url 'staff_tickets:delete' ticket.public_id %}" onsubmit="return confirm('آیا از حذف کامل و غیرقابلبازگشت این تیکت مطمئن هستید؟')">{% csrf_token %}<button type="submit" class="danger-button">حذف کامل تیکت</button></form></section>{% endif %}
|
{% if can_delete %}<section class="info-card danger-zone"><h3>حذف کامل تیکت</h3><p class="muted">تمام پیامها، فایلها و سوابق تیکت برای همیشه حذف میشوند.</p><form method="post" action="{% url 'staff_tickets:delete' ticket.public_id %}" onsubmit="return confirm('آیا از حذف کامل و غیرقابلبازگشت این تیکت مطمئن هستید؟')">{% csrf_token %}<button type="submit" class="danger-button">حذف کامل تیکت</button></form></section>{% endif %}
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -18,6 +18,21 @@ class TicketConversationPage(BasePage):
|
|||||||
assert "reply_result=reply" in self.page.url
|
assert "reply_result=reply" in self.page.url
|
||||||
expect(self.page.locator("[data-conversation]")).to_contain_text(body)
|
expect(self.page.locator("[data-conversation]")).to_contain_text(body)
|
||||||
|
|
||||||
|
def open_composer(self):
|
||||||
|
launch = self.page.locator("[data-composer-toggle]")
|
||||||
|
expect(launch).to_be_visible()
|
||||||
|
launch.click()
|
||||||
|
expect(self.page.locator("[data-composer-shell]:not(.is-collapsed)")).to_be_visible()
|
||||||
|
|
||||||
|
def expect_latest_message_above_composer(self):
|
||||||
|
latest = self.page.locator("[data-message]").last
|
||||||
|
expect(latest).to_be_visible()
|
||||||
|
latest.scroll_into_view_if_needed()
|
||||||
|
message_box = latest.bounding_box()
|
||||||
|
composer_box = self.page.locator("[data-composer-shell]").bounding_box()
|
||||||
|
assert message_box and composer_box
|
||||||
|
assert message_box["y"] + message_box["height"] <= composer_box["y"] + 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def chat_guidance(self):
|
def chat_guidance(self):
|
||||||
return self.page.locator(".chat-coach-floating")
|
return self.page.locator(".chat-coach-floating")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from apps.accounts.models import User
|
from apps.accounts.models import User
|
||||||
from apps.customers.models import CustomerProfile
|
from apps.customers.models import CustomerProfile
|
||||||
|
from apps.messaging.models import Message
|
||||||
from apps.staff.models import StaffProfile
|
from apps.staff.models import StaffProfile
|
||||||
from apps.tickets.models import TicketCategory
|
from apps.tickets.models import TicketCategory
|
||||||
from apps.tickets.services import create_ticket
|
from apps.tickets.services import create_ticket
|
||||||
@@ -76,3 +77,32 @@ def test_staff_sees_chat_guidance_after_two_rapid_replies(live_server, browser_p
|
|||||||
conversation.send_reply("پاسخ سوم کارشناس")
|
conversation.send_reply("پاسخ سوم کارشناس")
|
||||||
conversation.expect_guidance_visible()
|
conversation.expect_guidance_visible()
|
||||||
conversation.dismiss_guidance()
|
conversation.dismiss_guidance()
|
||||||
|
|
||||||
|
|
||||||
|
def test_customer_mobile_composer_never_covers_latest_message(live_server, browser_page):
|
||||||
|
category = TicketCategory.objects.create(title="مکاتبات موبایل", slug="e2e-customer-mobile-composer")
|
||||||
|
customer = User.objects.create_user(
|
||||||
|
username="e2e-mobile-customer",
|
||||||
|
mobile="09120009021",
|
||||||
|
password=PASSWORD,
|
||||||
|
account_type=User.AccountType.CUSTOMER,
|
||||||
|
)
|
||||||
|
CustomerProfile.objects.create(user=customer, first_name="مشتری", last_name="موبایل")
|
||||||
|
staff = User.objects.create_user(
|
||||||
|
username="e2e-mobile-staff",
|
||||||
|
mobile="09120009022",
|
||||||
|
account_type=User.AccountType.STAFF,
|
||||||
|
)
|
||||||
|
StaffProfile.objects.create(user=staff, display_name="کارشناس موبایل")
|
||||||
|
ticket = create_ticket(customer=customer, category=category, subject="کنترل composer مشتری", body="پیام آغازین")
|
||||||
|
for index in range(24):
|
||||||
|
Message.objects.create(ticket=ticket, author=staff, body=f"پاسخ طولانی کارشناس شماره {index + 1}")
|
||||||
|
|
||||||
|
with browser_page() as page:
|
||||||
|
page.set_viewport_size({"width": 390, "height": 844})
|
||||||
|
AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD)
|
||||||
|
conversation = TicketConversationPage(page, live_server.url)
|
||||||
|
conversation.goto(reverse("tickets:detail", args=[ticket.public_id]))
|
||||||
|
conversation.expect_latest_message_above_composer()
|
||||||
|
conversation.open_composer()
|
||||||
|
conversation.expect_latest_message_above_composer()
|
||||||
@@ -9,11 +9,13 @@ from playwright.sync_api import expect
|
|||||||
from apps.accounts.models import TicketMagicLink, User
|
from apps.accounts.models import TicketMagicLink, User
|
||||||
from apps.customers.models import ContactGroup, CustomerProfile
|
from apps.customers.models import ContactGroup, CustomerProfile
|
||||||
from apps.core.models import PageTitle, SiteBranding
|
from apps.core.models import PageTitle, SiteBranding
|
||||||
|
from apps.messaging.models import Message
|
||||||
from apps.notifications.models import NotificationJob, SMSTemplate
|
from apps.notifications.models import NotificationJob, SMSTemplate
|
||||||
from apps.staff.models import OrganizationalUnit, StaffProfile
|
from apps.staff.models import OrganizationalUnit, StaffProfile
|
||||||
from apps.tickets.models import TicketCategory
|
from apps.tickets.models import TicketCategory
|
||||||
from apps.tickets.services import create_ticket, dispatch_group_tickets
|
from apps.tickets.services import create_ticket, dispatch_group_tickets
|
||||||
from tests.e2e.pages.auth import AuthenticationPage
|
from tests.e2e.pages.auth import AuthenticationPage
|
||||||
|
from tests.e2e.pages.tickets import TicketConversationPage
|
||||||
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
|
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
|
||||||
@@ -153,6 +155,8 @@ def test_dispatch_report_presents_delivery_and_engagement_professionally(live_se
|
|||||||
delivery_description="0098SMS [کد 5]: پیامک به گوشی گیرنده رسیده است.",
|
delivery_description="0098SMS [کد 5]: پیامک به گوشی گیرنده رسیده است.",
|
||||||
delivery_check_attempts=2,delivery_checked_at=timezone.now(),delivery_next_check_at=None,
|
delivery_check_attempts=2,delivery_checked_at=timezone.now(),delivery_next_check_at=None,
|
||||||
)
|
)
|
||||||
|
for index in range(24):
|
||||||
|
Message.objects.create(ticket=ticket,author=staff,body=f"مکاتبه طولانی شماره {index + 1}")
|
||||||
links=[TicketMagicLink.issue_with_record(ticket,created_by=staff)[1] for _ in range(4)]
|
links=[TicketMagicLink.issue_with_record(ticket,created_by=staff)[1] for _ in range(4)]
|
||||||
links[0].view_count=2; links[0].used_at=timezone.now(); links[0].save(update_fields=["view_count","used_at"])
|
links[0].view_count=2; links[0].used_at=timezone.now(); links[0].save(update_fields=["view_count","used_at"])
|
||||||
|
|
||||||
@@ -190,6 +194,8 @@ def test_dispatch_report_presents_delivery_and_engagement_professionally(live_se
|
|||||||
assert box["x"] >= 0 and box["x"] + box["width"] <= 390
|
assert box["x"] >= 0 and box["x"] + box["width"] <= 390
|
||||||
assert box["y"] >= 0 and box["y"] + box["height"] <= 844
|
assert box["y"] >= 0 and box["y"] + box["height"] <= 844
|
||||||
assert page.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1")
|
assert page.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1")
|
||||||
|
conversation=TicketConversationPage(page,live_server.url)
|
||||||
|
conversation.expect_latest_message_above_composer()
|
||||||
launch.click()
|
launch.click()
|
||||||
close_composer = page.get_by_role("button", name="بستن", exact=True)
|
close_composer = page.get_by_role("button", name="بستن", exact=True)
|
||||||
expect(close_composer).to_be_visible()
|
expect(close_composer).to_be_visible()
|
||||||
@@ -197,5 +203,6 @@ def test_dispatch_report_presents_delivery_and_engagement_professionally(live_se
|
|||||||
assert close_box is not None
|
assert close_box is not None
|
||||||
assert close_box["x"] >= 0 and close_box["x"] + close_box["width"] <= 390
|
assert close_box["x"] >= 0 and close_box["x"] + close_box["width"] <= 390
|
||||||
assert close_box["y"] >= 0 and close_box["y"] + close_box["height"] <= 844
|
assert close_box["y"] >= 0 and close_box["y"] + close_box["height"] <= 844
|
||||||
|
conversation.expect_latest_message_above_composer()
|
||||||
dismiss_context.click()
|
dismiss_context.click()
|
||||||
expect(page.locator(".ticket-dispatch-context")).to_have_count(0)
|
expect(page.locator(".ticket-dispatch-context")).to_have_count(0)
|
||||||
+31
-1
@@ -8,6 +8,7 @@ from django.utils import timezone
|
|||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from apps.attachments.models import Attachment, AttachmentPolicy
|
from apps.attachments.models import Attachment, AttachmentPolicy
|
||||||
from apps.accounts.models import User, normalize_mobile
|
from apps.accounts.models import User, normalize_mobile
|
||||||
|
from apps.messaging.models import MessageReadReceipt
|
||||||
from apps.notifications.models import NotificationJob
|
from apps.notifications.models import NotificationJob
|
||||||
from apps.staff.models import OrganizationalUnit, StaffProfile
|
from apps.staff.models import OrganizationalUnit, StaffProfile
|
||||||
from apps.tickets.models import Ticket, TicketCategory
|
from apps.tickets.models import Ticket, TicketCategory
|
||||||
@@ -499,7 +500,11 @@ def test_due_unread_staff_reply_is_sent_once(category,customer,monkeypatch):
|
|||||||
from apps.integrations.sms.base import SMSResult
|
from apps.integrations.sms.base import SMSResult
|
||||||
from apps.notifications.management.commands import process_notifications
|
from apps.notifications.management.commands import process_notifications
|
||||||
a=make_staff("a"); ticket=create_ticket(customer=customer,category=category,subject="پاسخ خوانده نشده",selected_expert=a,body="متن")
|
a=make_staff("a"); ticket=create_ticket(customer=customer,category=category,subject="پاسخ خوانده نشده",selected_expert=a,body="متن")
|
||||||
reply_ticket(ticket_id=ticket.pk,user=a,body="پاسخ کارشناس")
|
message=reply_ticket(ticket_id=ticket.pk,user=a,body="پاسخ کارشناس")
|
||||||
|
observer=make_staff("observer")
|
||||||
|
other_customer=User.objects.create_user(username="other-sms-customer",mobile="09123334444",account_type="CUSTOMER")
|
||||||
|
MessageReadReceipt.objects.create(message=message,user=observer)
|
||||||
|
MessageReadReceipt.objects.create(message=message,user=other_customer)
|
||||||
job=NotificationJob.objects.get(ticket=ticket,event_type="STAFF_REPLY")
|
job=NotificationJob.objects.get(ticket=ticket,event_type="STAFF_REPLY")
|
||||||
NotificationJob.objects.filter(pk=job.pk).update(next_attempt_at=timezone.now()-timedelta(seconds=1))
|
NotificationJob.objects.filter(pk=job.pk).update(next_attempt_at=timezone.now()-timedelta(seconds=1))
|
||||||
ticket.last_activity_at=timezone.now()-timedelta(hours=1); ticket.save(update_fields=["last_activity_at"])
|
ticket.last_activity_at=timezone.now()-timedelta(hours=1); ticket.save(update_fields=["last_activity_at"])
|
||||||
@@ -514,6 +519,31 @@ def test_due_unread_staff_reply_is_sent_once(category,customer,monkeypatch):
|
|||||||
assert len(sent)==1
|
assert len(sent)==1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_due_staff_reply_is_cancelled_only_when_ticket_customer_has_seen_it(category,customer,monkeypatch):
|
||||||
|
from django.core.management import call_command
|
||||||
|
from apps.notifications.management.commands import process_notifications
|
||||||
|
|
||||||
|
staff=make_staff("a")
|
||||||
|
ticket=create_ticket(customer=customer,category=category,subject="پاسخ دیده شده",selected_expert=staff,body="متن")
|
||||||
|
message=reply_ticket(ticket_id=ticket.pk,user=staff,body="پاسخ کارشناس")
|
||||||
|
MessageReadReceipt.objects.create(message=message,user=customer)
|
||||||
|
job=NotificationJob.objects.get(ticket=ticket,event_type="STAFF_REPLY")
|
||||||
|
NotificationJob.objects.filter(pk=job.pk).update(next_attempt_at=timezone.now()-timedelta(seconds=1))
|
||||||
|
ticket.last_activity_at=timezone.now()-timedelta(hours=1)
|
||||||
|
ticket.save(update_fields=["last_activity_at"])
|
||||||
|
|
||||||
|
class Provider:
|
||||||
|
def send_message(self,mobile,message):
|
||||||
|
raise AssertionError("پیام دیدهشده توسط خود مشتری نباید به درگاه ارسال شود")
|
||||||
|
|
||||||
|
monkeypatch.setattr(process_notifications,"get_sms_provider",lambda:Provider())
|
||||||
|
call_command("process_notifications",once=True,job_id=job.pk)
|
||||||
|
job.refresh_from_db()
|
||||||
|
assert job.status==NotificationJob.Status.CANCELLED
|
||||||
|
assert job.last_error=="customer_read"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_every_failed_sms_gets_exactly_three_retries_three_minutes_apart(category,customer,monkeypatch,settings):
|
def test_every_failed_sms_gets_exactly_three_retries_three_minutes_apart(category,customer,monkeypatch,settings):
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
|
|||||||
+41
-1
@@ -252,6 +252,42 @@ def test_staff_reply_feedback_and_new_reply_action_remain_available(client):
|
|||||||
assert 'composer staff-composer is-collapsed' in content
|
assert 'composer staff-composer is-collapsed' in content
|
||||||
assert 'accept="image/*"' in content and "data-gallery-input" in content and "data-document-input" in content
|
assert 'accept="image/*"' in content and "data-gallery-input" in content and "data-document-input" in content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_staff_ticket_sms_status_is_compact_expandable_and_time_precise(client):
|
||||||
|
from apps.core.templatetags.jalali import jalali_datetime
|
||||||
|
from apps.notifications.models import NotificationJob
|
||||||
|
|
||||||
|
category=TicketCategory.objects.create(title="رهگیری پیامک",slug="sms-timeline")
|
||||||
|
customer=User.objects.create_user(username="sms-timeline-customer",mobile="09120000031",account_type="CUSTOMER")
|
||||||
|
expert=User.objects.create_user(username="sms-timeline-expert",mobile="09120000032",account_type="STAFF")
|
||||||
|
StaffProfile.objects.create(user=expert,display_name="کارشناس رهگیری")
|
||||||
|
expert.user_permissions.add(Permission.objects.get(content_type__app_label="tickets",codename="view_ticket"))
|
||||||
|
ticket=create_ticket(customer=customer,category=category,subject="نمایش دقیق وضعیت پیامک",selected_expert=expert,body="شرح")
|
||||||
|
job=NotificationJob.objects.get(ticket=ticket,event_type="CUSTOMER_TICKET_CREATED")
|
||||||
|
sent_at=timezone.now()-timedelta(minutes=4)
|
||||||
|
delivered_at=timezone.now()-timedelta(minutes=1)
|
||||||
|
NotificationJob.objects.filter(pk=job.pk).update(
|
||||||
|
status=NotificationJob.Status.SENT,
|
||||||
|
sent_at=sent_at,
|
||||||
|
provider_message_id="123456789",
|
||||||
|
delivery_status=NotificationJob.DeliveryStatus.DELIVERED,
|
||||||
|
delivery_status_code="5",
|
||||||
|
delivery_description="پیامک به گوشی گیرنده رسیده است.",
|
||||||
|
delivery_checked_at=delivered_at,
|
||||||
|
delivery_next_check_at=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
client.force_login(expert)
|
||||||
|
content=client.get(reverse("staff_tickets:detail",args=[ticket.public_id])).content.decode()
|
||||||
|
assert 'class="sms-delivery-item"' in content
|
||||||
|
assert 'aria-label="نمایش جزئیات زمانبندی پیامک"' in content
|
||||||
|
for label in ("ثبت درخواست ارسال","پذیرش توسط درگاه پیامک","تأیید رسیدن به گوشی"):
|
||||||
|
assert label in content
|
||||||
|
assert jalali_datetime(sent_at) in content
|
||||||
|
assert jalali_datetime(delivered_at) in content
|
||||||
|
assert "123456789" in content
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_staff_sidebar_hides_admin_link_and_shows_developer_credit(client):
|
def test_staff_sidebar_hides_admin_link_and_shows_developer_credit(client):
|
||||||
expert=User.objects.create_superuser(username="sidebar-admin",mobile="09384999655",password="test-pass",account_type="STAFF")
|
expert=User.objects.create_superuser(username="sidebar-admin",mobile="09384999655",password="test-pass",account_type="STAFF")
|
||||||
@@ -392,7 +428,7 @@ def test_ticket_polling_is_versioned_incremental_and_permission_safe(client):
|
|||||||
|
|
||||||
def test_ticket_polling_javascript_keeps_background_sync_and_recovers_reliably():
|
def test_ticket_polling_javascript_keeps_background_sync_and_recovers_reliably():
|
||||||
script=Path("static/js/conversation.js").read_text(encoding="utf-8")
|
script=Path("static/js/conversation.js").read_text(encoding="utf-8")
|
||||||
for feature in ("visibilitychange","document.visibilityState==='visible'","AbortController","pollRunning","nextPollAt","pollHeartbeat","setInterval(startPolling,1000)","cache:'no-store'","const restartPolling=()=>{stopPolling();startPolling()}","window.addEventListener('pageshow',restartPolling)","window.addEventListener('online',restartPolling)","data-scroll-to-submit","scrollIntoView","submit-attention"):
|
for feature in ("visibilitychange","document.visibilityState==='visible'","AbortController","pollRunning","nextPollAt","pollHeartbeat","setInterval(startPolling,1000)","cache:'no-store'","const restartPolling=()=>{stopPolling();startPolling()}","window.addEventListener('pageshow',restartPolling)","window.addEventListener('online',restartPolling)","data-scroll-to-submit","scrollIntoView","submit-attention","keepLatestVisible","requestAnimationFrame"):
|
||||||
assert feature in script
|
assert feature in script
|
||||||
assert "pollUrl&&navigator.onLine" not in script
|
assert "pollUrl&&navigator.onLine" not in script
|
||||||
assert "window.addEventListener('offline',stopPolling)" not in script
|
assert "window.addEventListener('offline',stopPolling)" not in script
|
||||||
@@ -403,6 +439,10 @@ def test_ticket_polling_javascript_keeps_background_sync_and_recovers_reliably()
|
|||||||
assert "scroller.scrollTo({top,behavior:'smooth'})" in script
|
assert "scroller.scrollTo({top,behavior:'smooth'})" in script
|
||||||
assert "data-message-id" in Path("templates/tickets/_messages.html").read_text(encoding="utf-8")
|
assert "data-message-id" in Path("templates/tickets/_messages.html").read_text(encoding="utf-8")
|
||||||
assert "acknowledgeVisibleMessages" in script and "document.visibilityState!=='visible'" in script
|
assert "acknowledgeVisibleMessages" in script and "document.visibilityState!=='visible'" in script
|
||||||
|
source_css=Path("static/src/tailwind.css").read_text(encoding="utf-8")
|
||||||
|
assert ".staff-ticket-workspace .composer.is-collapsed{position:fixed" not in source_css
|
||||||
|
assert ".staff-ticket-workspace .composer:not(.is-collapsed){position:fixed" not in source_css
|
||||||
|
assert "max-height:min(45dvh,28rem)" in source_css
|
||||||
for menu_feature in ("data-message-menu","data-message-menu-close","closeMessageMenu","event.key==='Escape'","!menu.contains(event.target)"):
|
for menu_feature in ("data-message-menu","data-message-menu-close","closeMessageMenu","event.key==='Escape'","!menu.contains(event.target)"):
|
||||||
assert menu_feature in script or menu_feature in Path("templates/tickets/_messages.html").read_text(encoding="utf-8")
|
assert menu_feature in script or menu_feature in Path("templates/tickets/_messages.html").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user