caterium-app/public/core/support-form.js
pavlov346346-source 5e72b23161
Correct support mailbox to support@caterium.ru (#42)
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.
2026-09-21 11:04:06 +03:00

88 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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);
})();