feat: production blocks 1-3 — invite links, server-side checkpoints, CRM↔client binding

This commit is contained in:
wasrusgen 2026-05-30 13:14:26 +03:00
parent dfad10bbff
commit ade80274ff
2 changed files with 18 additions and 4 deletions

View File

@ -227,7 +227,10 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
<script> <script>
const API="https://claude-83-172-150-111.sslip.io/elena"; const API="https://claude-83-172-150-111.sslip.io/elena";
let token=localStorage.getItem("cab_token"), state=null, cur=1; // Доступ: токен из ссылки-приглашения (?t=) имеет приоритет
const urlToken=new URLSearchParams(location.search).get("t");
if(urlToken)localStorage.setItem("cab_token",urlToken);
let token=urlToken||localStorage.getItem("cab_token"), state=null, cur=1;
const chat=document.getElementById("chat"), inp=document.getElementById("inp"); const chat=document.getElementById("chat"), inp=document.getElementById("inp");
const PCTS={1:20,2:40,3:60,4:80,5:100}; const PCTS={1:20,2:40,3:60,4:80,5:100};
function esc(s){return (s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")} function esc(s){return (s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}

View File

@ -188,9 +188,19 @@ const TABS=[
{id:"idef0",name:"Функции",icon:"🔧"}, {id:"idef0",name:"Функции",icon:"🔧"},
{id:"spec",name:"ТЗ",icon:"📋"} {id:"spec",name:"ТЗ",icon:"📋"}
]; ];
function approved(stage){return localStorage.getItem(`appr_${current}_${stage}`)==="1"} function approved(stage){return state.approvals && state.approvals[stage]}
function approve(stage){localStorage.setItem(`appr_${current}_${stage}`,"1");renderMain()} async function approve(stage){
function unapprove(stage){localStorage.removeItem(`appr_${current}_${stage}`);renderMain()} await fetch(`${API}/api/project/approve`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage,approved:true})});
state.approvals=state.approvals||{};state.approvals[stage]=new Date().toISOString();renderMain();
}
async function unapprove(stage){
await fetch(`${API}/api/project/approve`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage,approved:false})});
if(state.approvals)delete state.approvals[stage];renderMain();
}
function inviteLink(){
const url=`${location.origin}${location.pathname.replace('crm.html','cabinet.html')}?t=${current}`;
navigator.clipboard.writeText(url).then(()=>alert("Ссылка для клиента скопирована:\n\n"+url)).catch(()=>prompt("Ссылка для клиента:",url));
}
function renderMain(){ function renderMain(){
const p=projects.find(x=>x.token===current); const p=projects.find(x=>x.token===current);
@ -198,6 +208,7 @@ function renderMain(){
<div class="topbar"> <div class="topbar">
<div class="tb-av">${esc((state.client_name||'?')[0])}</div> <div class="tb-av">${esc((state.client_name||'?')[0])}</div>
<div><div class="tb-name">${esc(state.client_name||'Без имени')}</div><div class="tb-meta">${esc(state.niche||'')} · ${state.messages.length} сообщений</div></div> <div><div class="tb-name">${esc(state.client_name||'Без имени')}</div><div class="tb-meta">${esc(state.niche||'')} · ${state.messages.length} сообщений</div></div>
<button class="cp-btn cp-redo" style="margin-left:auto" onclick="inviteLink()">🔗 Ссылка клиенту</button>
<div class="tb-status">${esc(statusLabel(state.status))}</div> <div class="tb-status">${esc(statusLabel(state.status))}</div>
</div> </div>
<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}<span class="tab-check"></span></div>`).join("")}</div> <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}<span class="tab-check"></span></div>`).join("")}</div>