feat(chat): авто-обновление канала Консультант (polling 15с, GET /api/operator-chat)

This commit is contained in:
wasrusgen 2026-06-03 12:57:47 +03:00
parent 865b87e664
commit 4be49a25fb
2 changed files with 20 additions and 5 deletions

View File

@ -406,7 +406,7 @@ function go(n){
document.getElementById('si'+n).classList.add('active'); document.getElementById('si'+n).classList.add('active');
if(PCTS[n]!=null){document.getElementById('pbPct').textContent=PCTS[n]+'%';document.getElementById('pbFill').style.width=PCTS[n]+'%';} if(PCTS[n]!=null){document.getElementById('pbPct').textContent=PCTS[n]+'%';document.getElementById('pbFill').style.width=PCTS[n]+'%';}
if(n===1)requestAnimationFrame(scrollChatBottom); // sv1 уже видим — высота посчитана, прыгаем вниз без «пролёта» if(n===1)requestAnimationFrame(scrollChatBottom); // sv1 уже видим — высота посчитана, прыгаем вниз без «пролёта»
if(n===6)renderOpChat(); if(n===6){renderOpChat();startOpPoll();}else{stopOpPoll();}
if(n===3)renderDocs(); if(n===3)renderDocs();
if(n===4)renderAnalysis(); if(n===4)renderAnalysis();
if(n===5)renderSpecPane(); if(n===5)renderSpecPane();
@ -467,6 +467,10 @@ function renderOpChat(){const c=document.getElementById("opChat");if(!c)return;c
async function opSend(){const inp=document.getElementById("opInp");const t=inp.value.trim();if(!t)return;inp.value="";inp.style.height="auto";opMsg("user",t);state.operator_chat=state.operator_chat||[];state.operator_chat.push({role:"user",content:t}); async function opSend(){const inp=document.getElementById("opInp");const t=inp.value.trim();if(!t)return;inp.value="";inp.style.height="auto";opMsg("user",t);state.operator_chat=state.operator_chat||[];state.operator_chat.push({role:"user",content:t});
try{const r=await fetch(`${API}/api/operator-chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,message:t})});const d=await r.json();if(d.messages)state.operator_chat=d.messages;}catch(e){opMsg("elena","Не доставлено: "+e.message);} try{const r=await fetch(`${API}/api/operator-chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,message:t})});const d=await r.json();if(d.messages)state.operator_chat=d.messages;}catch(e){opMsg("elena","Не доставлено: "+e.message);}
} }
let opPollTimer=null;
async function opPollOnce(){try{const r=await fetch(`${API}/api/operator-chat?token=${encodeURIComponent(token)}`);const d=await r.json();if(d.messages&&d.messages.length!==(state.operator_chat||[]).length){state.operator_chat=d.messages;renderOpChat();}}catch(e){}}
function startOpPoll(){stopOpPoll();opPollOnce();opPollTimer=setInterval(opPollOnce,15000);}
function stopOpPoll(){if(opPollTimer){clearInterval(opPollTimer);opPollTimer=null;}}
/* ── Спросить Елену (этапы 3-5) ── */ /* ── Спросить Елену (этапы 3-5) ── */
const STAGE_LBL={3:"о документах",4:"о стратегии и модели",5:"о ТЗ и плане"}; const STAGE_LBL={3:"о документах",4:"о стратегии и модели",5:"о ТЗ и плане"};
function toggleAsk(){document.getElementById("askDock").classList.toggle("open")} function toggleAsk(){document.getElementById("askDock").classList.toggle("open")}

View File

@ -320,9 +320,9 @@ async function submitNewClient(){
await loadProjects();openClient(d.token);toast('Клиент «'+name+'» создан','ok'); await loadProjects();openClient(d.token);toast('Клиент «'+name+'» создан','ok');
}catch(e){toast('Ошибка: '+e.message,'err');btn.disabled=false;btn.textContent='Создать →';} }catch(e){toast('Ошибка: '+e.message,'err');btn.disabled=false;btn.textContent='Создать →';}
} }
function setView(v){view=v;current=null;if(window.innerWidth<=680)closeSb();document.getElementById("nav-dash").classList.toggle("active",v==="dashboard");document.getElementById("nav-pipe").classList.toggle("active",v==="pipeline");renderClientList();render();} function setView(v){stopCrmChatPoll();view=v;current=null;if(window.innerWidth<=680)closeSb();document.getElementById("nav-dash").classList.toggle("active",v==="dashboard");document.getElementById("nav-pipe").classList.toggle("active",v==="pipeline");renderClientList();render();}
async function openClient(token){ async function openClient(token){
current=token;view="client";mainTab="deal";editPayIdx=-1;if(window.innerWidth<=680)closeSb();document.querySelectorAll(".nav-item").forEach(n=>n.classList.remove("active")); stopCrmChatPoll();current=token;view="client";mainTab="deal";editPayIdx=-1;if(window.innerWidth<=680)closeSb();document.querySelectorAll(".nav-item").forEach(n=>n.classList.remove("active"));
const r=await fetch(`${API}/api/project/${token}`);state=await r.json();renderClientList();render();syncPaymentReminders(); const r=await fetch(`${API}/api/project/${token}`);state=await r.json();renderClientList();render();syncPaymentReminders();
} }
function render(){ function render(){
@ -527,7 +527,7 @@ function renderClient(){
<div id="mainPanel"></div>`; <div id="mainPanel"></div>`;
renderMainPanel(); renderMainPanel();
} }
function setMainTab(t){mainTab=t;editPayIdx=-1;renderMainPanel();document.querySelectorAll('.mtab').forEach((b,i)=>b.classList.toggle('active',MAINTABS[i].id===mainTab));} function setMainTab(t){if(t!=='chat')stopCrmChatPoll();mainTab=t;editPayIdx=-1;renderMainPanel();document.querySelectorAll('.mtab').forEach((b,i)=>b.classList.toggle('active',MAINTABS[i].id===mainTab));}
function renderDocsTab(){ function renderDocsTab(){
const docs=state.documents||[]; const docs=state.documents||[];
let h=`<div onclick="document.getElementById('opFile').click()" ondragover="event.preventDefault();this.style.background='#ECFDF5'" ondragleave="this.style.background='#fff'" ondrop="opDropFiles(event)" style="border:2px dashed var(--border);border-radius:12px;padding:20px;text-align:center;cursor:pointer;margin-bottom:14px;background:#fff;transition:background .15s"> let h=`<div onclick="document.getElementById('opFile').click()" ondragover="event.preventDefault();this.style.background='#ECFDF5'" ondragleave="this.style.background='#fff'" ondrop="opDropFiles(event)" style="border:2px dashed var(--border);border-radius:12px;padding:20px;text-align:center;cursor:pointer;margin-bottom:14px;background:#fff;transition:background .15s">
@ -579,6 +579,17 @@ async function opChatSend(){
localStorage.setItem('opseen_'+current,(state.operator_chat||[]).filter(m=>m.role==='user').length); localStorage.setItem('opseen_'+current,(state.operator_chat||[]).filter(m=>m.role==='user').length);
}catch(e){toast('Ошибка: '+e.message,'err');} }catch(e){toast('Ошибка: '+e.message,'err');}
} }
let crmChatPoll=null;
async function crmChatPollOnce(){
if(!current)return;
try{const r=await fetch(`${API}/api/operator-chat?token=${encodeURIComponent(current)}`);const d=await r.json();
if(d.messages&&d.messages.length!==(state.operator_chat||[]).length){state.operator_chat=d.messages;
if(mainTab==='chat'){renderOpChatTab();localStorage.setItem('opseen_'+current,d.messages.filter(m=>m.role==='user').length);}
}
}catch(e){}
}
function startCrmChatPoll(){stopCrmChatPoll();crmChatPoll=setInterval(crmChatPollOnce,15000);}
function stopCrmChatPoll(){if(crmChatPoll){clearInterval(crmChatPoll);crmChatPoll=null;}}
const DEV_STAGE={canvas:"📊 Стратегия",idef0:"🔧 Функции",spec:"📋 ТЗ",documents:"📁 Документы",methods:"🎯 Методологии",interview:"💬 Интервью"}; const DEV_STAGE={canvas:"📊 Стратегия",idef0:"🔧 Функции",spec:"📋 ТЗ",documents:"📁 Документы",methods:"🎯 Методологии",interview:"💬 Интервью"};
function renderDeviations(){ function renderDeviations(){
const dev=state.deviations||[]; const dev=state.deviations||[];
@ -661,7 +672,7 @@ function renderMainPanel(){
else if(mainTab==="tasks"){p.innerHTML=`<div id="tasksBox"></div>`;renderTasks();} else if(mainTab==="tasks"){p.innerHTML=`<div id="tasksBox"></div>`;renderTasks();}
else if(mainTab==="analysis"){p.innerHTML=`<div class="tabs">${TABS.map(t=>`<div class="tab ${t.id===activeTab?'active':''} ${approved(t.id)?'done':''}" onclick="setTab('${t.id}')">${t.icon} ${t.name}</div>`).join("")}</div><div id="tabContent"></div>`;renderTab();} else if(mainTab==="analysis"){p.innerHTML=`<div class="tabs">${TABS.map(t=>`<div class="tab ${t.id===activeTab?'active':''} ${approved(t.id)?'done':''}" onclick="setTab('${t.id}')">${t.icon} ${t.name}</div>`).join("")}</div><div id="tabContent"></div>`;renderTab();}
else if(mainTab==="docs"){p.innerHTML=renderDocsTab();} else if(mainTab==="docs"){p.innerHTML=renderDocsTab();}
else if(mainTab==="chat"){p.innerHTML=`<div id="opChatBox"></div>`;renderOpChatTab();localStorage.setItem('opseen_'+current,(state.operator_chat||[]).filter(m=>m.role==='user').length);document.querySelectorAll('.mtab').forEach((b,i)=>{if(MAINTABS[i].id==='chat'){const bd=b.querySelector('.badge');if(bd)bd.remove();}});} else if(mainTab==="chat"){p.innerHTML=`<div id="opChatBox"></div>`;renderOpChatTab();localStorage.setItem('opseen_'+current,(state.operator_chat||[]).filter(m=>m.role==='user').length);document.querySelectorAll('.mtab').forEach((b,i)=>{if(MAINTABS[i].id==='chat'){const bd=b.querySelector('.badge');if(bd)bd.remove();}});startCrmChatPoll();}
else if(mainTab==="deviations"){p.innerHTML=renderDeviations();} else if(mainTab==="deviations"){p.innerHTML=renderDeviations();}
} }
function inviteTelegram(){const url=`https://t.me/wasrusgen1_consulting_bot?start=${current}`;navigator.clipboard.writeText(url).then(()=>alert("Ссылка для Telegram скопирована:\n\n"+url+"\n\nКлиент откроет кабинет прямо в Telegram.")).catch(()=>prompt("Ссылка для Telegram:",url));} function inviteTelegram(){const url=`https://t.me/wasrusgen1_consulting_bot?start=${current}`;navigator.clipboard.writeText(url).then(()=>alert("Ссылка для Telegram скопирована:\n\n"+url+"\n\nКлиент откроет кабинет прямо в Telegram.")).catch(()=>prompt("Ссылка для Telegram:",url));}