Apply the user's explicit address correction to the PHP recipient, Help links, contact form and tests. Refresh the form URL and PWA cache. Exact-recipient PHP tests and support/Help browser tests passed in isolated run 35575721587. No real mail sent, delivery not claimed. Keep standard main QA and production publication gates unchanged; no auth, database or unrelated feature changes.
88 lines
11 KiB
JavaScript
88 lines
11 KiB
JavaScript
/* Human support: explicit submit only. Do not serialize the SDK or application state. */
|
||
(()=>{
|
||
'use strict';
|
||
if(window.CateriumSupportForm)return;
|
||
const EMAIL='support@caterium.ru',ENDPOINT=new URL('../api/support.php',document.currentScript.src).href;
|
||
const identity=()=>`${window.SunCloudV2?.getSession?.()?.user?.id||''}:${window.SunCloudV2?.getWorkspace?.()?.id||''}`;
|
||
let root,form,tab,csrf='',busy=false,controller=null,scope=identity(),generation=0,lastPayload='',requestId='';
|
||
const $=id=>root?.querySelector('#'+id);
|
||
const uuid=()=>typeof crypto.randomUUID==='function'?crypto.randomUUID():[4,2,2,2,6].map(n=>Array.from(crypto.getRandomValues(new Uint8Array(n)),v=>v.toString(16).padStart(2,'0')).join('')).join('-');
|
||
function status(message,error=false){$('ctContactStatus').textContent=message;$('ctContactStatus').setAttribute('role',error?'alert':'status');$('ctContactStatus').classList.toggle('ct-contact-error',error);}
|
||
function show(){
|
||
if(!root)return;
|
||
$('ctHelpGuide').hidden=true;$('ctHelpSupport').hidden=true;$('ctContactSection').hidden=false;
|
||
$('ctHelpGuideTab').setAttribute('aria-pressed','false');$('ctHelpSupportTab').setAttribute('aria-pressed','false');tab.setAttribute('aria-pressed','true');
|
||
root.querySelector('.ct-help-content').scrollTop=0;
|
||
const user=window.SunCloudV2?.getSession?.()?.user;
|
||
if(!form.dataset.prefilled){$('ctContactEmail').value=typeof user?.email==='string'?user.email:'';$('ctContactName').value=typeof user?.user_metadata?.name==='string'?user.user_metadata.name:'';form.dataset.prefilled='1';}
|
||
$('ctContactName').focus({preventScroll:true});
|
||
}
|
||
async function request(options={}){
|
||
const activeController=new AbortController();controller=activeController;const timer=setTimeout(()=>activeController.abort(),20000);
|
||
try{
|
||
const response=await fetch(ENDPOINT,{credentials:'same-origin',cache:'no-store',signal:activeController.signal,...options});
|
||
let value;try{value=await response.json();}catch(_){throw new Error('Сервер не подтвердил отправку. Текст остаётся в форме.');}
|
||
if(!response.ok){if(response.status===403)csrf='';throw new Error(typeof value.message==='string'?value.message:'Не удалось отправить сообщение.');}
|
||
return value;
|
||
}finally{clearTimeout(timer);if(controller===activeController)controller=null;}
|
||
}
|
||
async function send(event){
|
||
event.preventDefault();if(busy||!form.reportValidity())return;
|
||
const ticket=generation,who=identity();busy=true;form.setAttribute('aria-busy','true');
|
||
const payload={name:$('ctContactName').value.trim(),email:$('ctContactEmail').value.trim(),topic:$('ctContactTopic').value,subject:$('ctContactSubject').value.trim(),message:$('ctContactMessage').value.trim(),website:$('ctContactWebsite').value};
|
||
if($('ctContactDiagnostics').checked){
|
||
payload.device=String(navigator.userAgent).slice(0,600)+`; экран ${innerWidth} × ${innerHeight}`;
|
||
payload.section=String(document.querySelector('header nav button.on')?.textContent||'Вход').trim().slice(0,100);
|
||
payload.release=String(window.SunPerformance?.VERSION||'').slice(0,60);
|
||
}
|
||
const fingerprint=JSON.stringify(payload);if(fingerprint!==lastPayload||!requestId){lastPayload=fingerprint;requestId=uuid();}
|
||
status('Отправляю обращение…');form.querySelectorAll('input,textarea,select,button').forEach(e=>e.disabled=true);
|
||
try{
|
||
if(!csrf){const setup=await request();if(typeof setup.csrf!=='string'||setup.recipient!==EMAIL)throw new Error('Не удалось подготовить защищённую отправку.');csrf=setup.csrf;}
|
||
if(ticket!==generation||who!==identity())return;
|
||
const answer=await request({method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({...payload,csrf,request_id:requestId})});
|
||
if(ticket!==generation||who!==identity())return;
|
||
if(answer.ok!==true||answer.status!=='accepted'||!/^SUP-[0-9]{8}-[A-F0-9]{12}$/.test(answer.id||''))throw new Error('Результат отправки не подтверждён. Текст остаётся в форме.');
|
||
status(`Обращение ${answer.id} принято почтовым сервером для отправки на ${EMAIL}. Ответ поддержки придёт на ${payload.email}.`);
|
||
$('ctContactMessage').value='';$('ctContactSubject').value='';lastPayload='';requestId='';
|
||
}catch(error){
|
||
if(ticket===generation&&who===identity())status(error.name==='AbortError'?'Не удалось дождаться подтверждения. Текст сохранён в форме. Повторная отправка этого же сообщения не создаст дубликат в течение 48 часов.':String(error.message||'Не удалось отправить сообщение.'),true);
|
||
}finally{
|
||
if(ticket===generation){busy=false;form.setAttribute('aria-busy','false');form.querySelectorAll('input,textarea,select,button').forEach(e=>e.disabled=false);}
|
||
}
|
||
}
|
||
function attach(dialog){
|
||
if(root===dialog)return;
|
||
root=dialog;scope=identity();
|
||
const style=document.createElement('style');style.textContent=`
|
||
#ctHelpDialog .ct-help-modes{flex-wrap:wrap}#ctHelpDialog #ctContactTab{font-weight:700}
|
||
#ctHelpDialog .ct-contact-cta{padding:16px;border:1px solid #d1c49d;border-radius:12px;background:#f8f4e7;margin:18px 0}
|
||
#ctHelpDialog .ct-contact-cta p{margin:0 0 10px}#ctHelpDialog .ct-contact-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
||
#ctHelpDialog .ct-contact-wide{grid-column:1/-1}#ctHelpDialog #ctContactForm textarea{font:16px/1.5 Arial,sans-serif;resize:vertical;width:100%;min-height:150px;padding:12px;border:1px solid #cdd5cb;border-radius:10px;background:#fff;color:#263d31}
|
||
#ctHelpDialog #ctContactForm textarea:focus-visible{outline:3px solid #b39b47;outline-offset:2px}#ctHelpDialog .ct-contact-check{display:flex;flex-direction:row;align-items:flex-start;font-size:13px;gap:8px}
|
||
#ctHelpDialog .ct-contact-check input{width:18px;height:18px;min-height:18px;flex:0 0 18px;margin:2px 0 0}
|
||
#ctHelpDialog .ct-contact-note{font-size:12px;color:#617166}#ctHelpDialog #ctContactSend{background:#304f3d;color:#fff;justify-self:start}
|
||
#ctHelpDialog [disabled]{cursor:wait;opacity:.65}#ctHelpDialog .ct-contact-error{color:#9e3028}#ctHelpDialog #ctContactStatus:empty{display:none}
|
||
#ctHelpDialog .ct-contact-hp{position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden}#ctHelpDialog #ctContactStatus{overflow-wrap:anywhere}
|
||
@media(max-width:600px){#ctHelpDialog .ct-contact-grid{grid-template-columns:minmax(0,1fr)}#ctHelpDialog #ctContactSend{width:100%}}
|
||
`;document.head.append(style);
|
||
tab=document.createElement('button');tab.id='ctContactTab';tab.type='button';tab.textContent='Написать в поддержку';tab.setAttribute('aria-pressed','false');root.querySelector('.ct-help-modes').append(tab);tab.onclick=show;
|
||
const section=document.createElement('section');section.id='ctContactSection';section.hidden=true;
|
||
section.innerHTML=`<h3>Не нашли ответ или возникла проблема?</h3><p>Опишите вопрос — обращение будет направлено на <a href="mailto:${EMAIL}">${EMAIL}</a>. Для ответа укажите свою почту.</p><form id="ctContactForm"><div class="ct-contact-grid"><label>Ваше имя<input id="ctContactName" name="name" autocomplete="name" maxlength="60" required></label><label>Ваш email для ответа<input id="ctContactEmail" name="email" type="email" autocomplete="email" maxlength="254" required></label><label>Что случилось?<select id="ctContactTopic" name="topic"><option value="question">Вопрос по приложению</option><option value="problem">Ошибка или проблема</option><option value="access">Вход и подписка</option><option value="suggestion">Предложение</option><option value="other">Другое</option></select></label><label>Тема обращения<input id="ctContactSubject" name="subject" maxlength="90" required placeholder="Например: не получается открыть PDF"></label><label class="ct-contact-wide">Ваш вопрос или описание проблемы<textarea id="ctContactMessage" name="message" rows="6" maxlength="4000" required placeholder="Что вы хотели сделать? Что произошло? Какие действия уже пробовали?"></textarea></label><label class="ct-contact-check ct-contact-wide"><input id="ctContactDiagnostics" type="checkbox"><span>Добавить сведения об устройстве: браузер, размер экрана, открытый раздел и версия приложения. Данные заказов и клиентов не отправляются.</span></label><div class="ct-contact-hp" aria-hidden="true"><label>Не заполняйте<input id="ctContactWebsite" name="website" tabindex="-1" autocomplete="off"></label></div><p class="ct-contact-note ct-contact-wide">Не указывайте пароли, коды подтверждения, секретные ключи и лишние персональные данные. При нажатии «Отправить» имя, email и сообщение передаются службе поддержки. Форма не открывает почтовое приложение.</p><button id="ctContactSend" class="ct-contact-wide" type="submit">Отправить в поддержку</button></div></form><p id="ctContactStatus" role="status" aria-live="polite"></p><p class="ct-contact-note">Если отправка через форму недоступна, напишите напрямую: <a href="mailto:${EMAIL}">${EMAIL}</a>.</p>`;
|
||
root.querySelector('.ct-help-content').append(section);form=$('ctContactForm');form.addEventListener('submit',send);
|
||
for(const id of ['ctHelpGuide','ctHelpSupport']){
|
||
const cta=document.createElement('div');cta.className='ct-contact-cta';cta.innerHTML='<p><b>Не нашли ответ? Мы поможем разобраться.</b></p><button type="button" data-human-support>Написать в поддержку</button>';
|
||
$(id).append(cta);cta.querySelector('button').onclick=show;
|
||
}
|
||
root.addEventListener('click',event=>{if(event.target.closest('#ctHelpGuideTab,#ctHelpSupportTab')){section.hidden=true;tab.setAttribute('aria-pressed','false');}});
|
||
}
|
||
function resetIfChanged(){
|
||
const next=identity();if(next===scope)return;scope=next;generation++;controller?.abort();csrf='';lastPayload='';requestId='';busy=false;
|
||
if(form){form.reset();delete form.dataset.prefilled;form.setAttribute('aria-busy','false');form.querySelectorAll('[disabled]').forEach(e=>e.disabled=false);status('');}
|
||
}
|
||
window.addEventListener('sun:cloud-permissions-changed',resetIfChanged);
|
||
window.addEventListener('sun:cloud-tenant-changing',()=>{scope='';resetIfChanged();});
|
||
window.CateriumSupportForm=Object.freeze({attach});
|
||
const existing=document.getElementById('ctHelpDialog');if(existing)attach(existing);
|
||
})();
|