118 lines
4.9 KiB
JavaScript
118 lines
4.9 KiB
JavaScript
function showToast(text, type='info', duration=4000) {
|
|
const wrap=document.getElementById('toast-wrap');
|
|
const el=document.createElement('div');
|
|
const cls={mark:'',note:'success',warn:'warn',reload:'info','mention-t':'mention',success:'success',info:'info',warn:'warn',mention:'mention'}[type]||'';
|
|
el.className=`toast ${cls}`; el.textContent=text;
|
|
wrap.appendChild(el);
|
|
setTimeout(()=>el.remove(),duration);
|
|
}
|
|
|
|
function playMentionSound() {
|
|
try {
|
|
const ctx=new(window.AudioContext||window.webkitAudioContext)();
|
|
const o=ctx.createOscillator(),g=ctx.createGain();
|
|
o.connect(g);g.connect(ctx.destination);
|
|
o.type='sine';o.frequency.value=880;
|
|
g.gain.setValueAtTime(0.25,ctx.currentTime);
|
|
g.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.35);
|
|
o.start(ctx.currentTime);o.stop(ctx.currentTime+0.35);
|
|
} catch {}
|
|
}
|
|
|
|
let sseSource=null;
|
|
let sseReconnectTimer=null;
|
|
function startSSE() {
|
|
if(sseReconnectTimer){clearTimeout(sseReconnectTimer);sseReconnectTimer=null;}
|
|
if(sseSource){sseSource.close();sseSource=null;}
|
|
if(!authToken) return;
|
|
sseSource=new EventSource(`/api/events?token=${encodeURIComponent(authToken)}`);
|
|
|
|
sseSource.addEventListener('mark',e=>{
|
|
const d=JSON.parse(e.data);
|
|
if(!marksCache[d.task_id]) marksCache[d.task_id]={};
|
|
marksCache[d.task_id][d.mark_type]={active:d.value,by:d.by,at:d.at};
|
|
// Сбрасываем все метки, которые сервер очистил в том же запросе
|
|
(d.cleared_marks||[]).forEach(m=>{
|
|
marksCache[d.task_id][m]={...(marksCache[d.task_id][m]||{}),active:false};
|
|
});
|
|
// Тост только для действий других пользователей
|
|
if(d.by!==currentUser?.username){
|
|
const markCfg=MARKS_CONFIG.find(m=>m.key===d.mark_type);
|
|
const label=markCfg?markCfg.sseLabel:d.mark_type;
|
|
showToast(`👤 ${d.by}: ${d.value?`✓ «${label}»`:`снял «${label}»`} #${taskNumFromId(d.task_id)}`,'info');
|
|
}
|
|
renderTable();
|
|
refreshDetailIfOpen(d.task_id);
|
|
});
|
|
|
|
sseSource.addEventListener('note',e=>{
|
|
const d=JSON.parse(e.data);
|
|
if(d.text) notesCache[d.task_id]={text:d.text,by:d.by,at:d.at};
|
|
else delete notesCache[d.task_id];
|
|
// Тост только для действий других пользователей
|
|
if(d.by!==currentUser?.username){
|
|
showToast(`📝 ${d.by}: ${d.text?'обновил заметку':'удалил заметку'} #${taskNumFromId(d.task_id)}`,'success');
|
|
}
|
|
renderTable();
|
|
refreshDetailIfOpen(d.task_id);
|
|
});
|
|
|
|
sseSource.addEventListener('comment_new',e=>{
|
|
const d=JSON.parse(e.data);
|
|
const tid=String(d.comment.task_id);
|
|
if(!commentsCache[tid]) commentsCache[tid]=[];
|
|
commentsCache[tid].push(d.comment);
|
|
if(typeof commentCounts==='object') commentCounts[tid]=(commentsCache[tid]||[]).length;
|
|
showToast(`💬 ${d.comment.author}: комментарий к #${d.task_number}`,'success');
|
|
renderTable();
|
|
refreshDetailIfOpen(tid);
|
|
});
|
|
|
|
sseSource.addEventListener('comment_edit',e=>{
|
|
const d=JSON.parse(e.data);
|
|
const tid=String(d.comment.task_id);
|
|
if(commentsCache[tid]) commentsCache[tid]=commentsCache[tid].map(c=>c.id===d.comment.id?d.comment:c);
|
|
refreshDetailIfOpen(tid);
|
|
});
|
|
|
|
sseSource.addEventListener('comment_delete',e=>{
|
|
const d=JSON.parse(e.data);
|
|
const tid=String(d.task_id);
|
|
if(commentsCache[tid]) commentsCache[tid]=commentsCache[tid].filter(c=>c.id!==d.comment_id);
|
|
if(typeof commentCounts==='object') commentCounts[tid]=(commentsCache[tid]||[]).length;
|
|
renderTable();
|
|
refreshDetailIfOpen(tid);
|
|
});
|
|
|
|
sseSource.addEventListener('tasks_updated',e=>{
|
|
const d=JSON.parse(e.data);
|
|
const incomingVersion=Number(d.cache_version||0);
|
|
if(incomingVersion&&incomingVersion===lastKnownCacheVersion) return;
|
|
if(incomingVersion) lastKnownCacheVersion=incomingVersion;
|
|
showToast(`🔄 Обновление: ${d.count} заявок`,'info',4000);
|
|
reloadData();
|
|
});
|
|
|
|
sseSource.addEventListener('mention',e=>{
|
|
const d=JSON.parse(e.data);
|
|
mentionsData.unshift({task_id:d.task_id,mentioned_user:currentUser.username,mentioned_by:d.by,created_at:d.at,read_at:null});
|
|
mentionUnread=d.unread_count;
|
|
renderMentionsBell();
|
|
playMentionSound();
|
|
showToast(`🔔 ${d.by} упомянул вас в заявке #${d.task_number}`,'mention',8000);
|
|
});
|
|
|
|
sseSource.onerror=()=>{
|
|
// Браузер сам переподключается при CONNECTING (0/1), нам нужно только при CLOSED (2)
|
|
if(!sseSource || sseSource.readyState !== EventSource.CLOSED) return;
|
|
if(sseReconnectTimer) return;
|
|
sseReconnectTimer=setTimeout(()=>{
|
|
sseReconnectTimer=null;
|
|
if(authToken) startSSE();
|
|
},5000);
|
|
};
|
|
}
|
|
function stopSSE(){
|
|
if(sseReconnectTimer){clearTimeout(sseReconnectTimer);sseReconnectTimer=null;}
|
|
if(sseSource){sseSource.close();sseSource=null;}
|
|
}
|