mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 22:24:46 +00:00
brand: убраны ВСЕ эмодзи -> Lucide-иконки по брендбуку (статус-бейджи, чипы, кнопки)
This commit is contained in:
parent
7bfedb3b8d
commit
0a335d0075
@ -298,7 +298,7 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
|
|||||||
<div style="max-width:600px">
|
<div style="max-width:600px">
|
||||||
<div style="margin-bottom:18px"><label class="pf-l">Ваше имя или название компании</label><input class="pf-i" id="pfName" placeholder="Например: Игорь / ООО «Автодом»"></div>
|
<div style="margin-bottom:18px"><label class="pf-l">Ваше имя или название компании</label><input class="pf-i" id="pfName" placeholder="Например: Игорь / ООО «Автодом»"></div>
|
||||||
<div style="margin-bottom:18px"><label class="pf-l">Сфера деятельности</label><input class="pf-i" id="pfNiche" placeholder="Например: автосервис, нутрициология, швейное производство"></div>
|
<div style="margin-bottom:18px"><label class="pf-l">Сфера деятельности</label><input class="pf-i" id="pfNiche" placeholder="Например: автосервис, нутрициология, швейное производство"></div>
|
||||||
<div style="margin-bottom:22px"><label class="pf-l" style="display:flex;align-items:center;justify-content:space-between">Описание деятельности <button type="button" id="pfMic" class="mic" onclick="toggleMic('pfDesc','pfMic')" title="Надиктовать" style="width:30px;height:30px;border-radius:9px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button></label><textarea class="pf-t" id="pfDesc" placeholder="Чем занимаетесь, сколько человек, как сейчас всё устроено, что беспокоит. Можно надиктовать 🎤"></textarea></div>
|
<div style="margin-bottom:22px"><label class="pf-l" style="display:flex;align-items:center;justify-content:space-between">Описание деятельности <button type="button" id="pfMic" class="mic" onclick="toggleMic('pfDesc','pfMic')" title="Надиктовать" style="width:30px;height:30px;border-radius:9px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button></label><textarea class="pf-t" id="pfDesc" placeholder="Чем занимаетесь, сколько человек, как сейчас всё устроено, что беспокоит. Можно надиктовать голосом"></textarea></div>
|
||||||
<button class="btn btn-p" id="pfSave" onclick="saveProfile()" style="padding:12px 24px;font-size:14px">Сохранить и начать с Еленой →</button>
|
<button class="btn btn-p" id="pfSave" onclick="saveProfile()" style="padding:12px 24px;font-size:14px">Сохранить и начать с Еленой →</button>
|
||||||
</div>
|
</div>
|
||||||
</div></div>
|
</div></div>
|
||||||
@ -406,7 +406,12 @@ const ICONS={
|
|||||||
receipt:'<path d="M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1-2-1z"/><path d="M16 8H8M16 12H8M13 16H8"/>',
|
receipt:'<path d="M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1-2-1z"/><path d="M16 8H8M16 12H8M13 16H8"/>',
|
||||||
search:'<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
|
search:'<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
|
||||||
target:'<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
|
target:'<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
|
||||||
send:'<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>'
|
send:'<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>',
|
||||||
|
close:'<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
||||||
|
checkCircle:'<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
|
||||||
|
sparkle:'<path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1"/>',
|
||||||
|
gift:'<polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/>',
|
||||||
|
dot:'<circle cx="12" cy="12" r="5"/>'
|
||||||
};
|
};
|
||||||
function ic(n,s,sw){s=s||20;return '<svg style="display:inline-block;vertical-align:middle" width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="'+(sw||1.75)+'" stroke-linecap="round" stroke-linejoin="round">'+(ICONS[n]||'')+'</svg>';}
|
function ic(n,s,sw){s=s||20;return '<svg style="display:inline-block;vertical-align:middle" width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="'+(sw||1.75)+'" stroke-linecap="round" stroke-linejoin="round">'+(ICONS[n]||'')+'</svg>';}
|
||||||
|
|
||||||
@ -494,7 +499,7 @@ async function opPollOnce(){try{const r=await fetch(`${API}/api/operator-chat?to
|
|||||||
function startOpPoll(){stopOpPoll();opPollOnce();opPollTimer=setInterval(opPollOnce,15000);}
|
function startOpPoll(){stopOpPoll();opPollOnce();opPollTimer=setInterval(opPollOnce,15000);}
|
||||||
function stopOpPoll(){if(opPollTimer){clearInterval(opPollTimer);opPollTimer=null;}}
|
function stopOpPoll(){if(opPollTimer){clearInterval(opPollTimer);opPollTimer=null;}}
|
||||||
/* ── Предложения клиента ── */
|
/* ── Предложения клиента ── */
|
||||||
const SUG_ST={new:['🆕 Новое','#EFF6FF','#2563EB'],discussion:['💬 На обсуждении','#FEF3C7','#92400E'],accepted:['✅ Принято','#D1FAE5','#047857'],rejected:['❌ Отклонено','#FEE2E2','#B91C1C']};
|
const SUG_ST={new:[ic('dot',12)+' Новое','#EFF6FF','#2563EB'],discussion:[ic('message',12)+' На обсуждении','#FEF3C7','#92400E'],accepted:[ic('checkCircle',12)+' Принято','#D1FAE5','#047857'],rejected:[ic('close',12)+' Отклонено','#FEE2E2','#B91C1C']};
|
||||||
function openIdeas(){
|
function openIdeas(){
|
||||||
const sgs=state.suggestions||[];
|
const sgs=state.suggestions||[];
|
||||||
const list=sgs.length?sgs.map(s=>{const st=SUG_ST[s.status]||SUG_ST.new;return `<div style="border:1px solid var(--border);border-radius:10px;padding:10px 12px;margin-bottom:8px"><div style="font-size:13px">${esc(s.text)}</div><div style="margin-top:6px;display:flex;align-items:center;gap:8px;flex-wrap:wrap"><span style="font-size:11px;font-weight:700;color:${st[2]};background:${st[1]};padding:2px 8px;border-radius:6px">${st[0]}</span>${s.decision?`<span style="font-size:11px;color:#6B7280">— ${esc(s.decision)}</span>`:''}</div></div>`}).join(''):'<div style="font-size:13px;color:#9ca3af;text-align:center;padding:10px">Пока нет. Предложите первую идею — мы рассмотрим её.</div>';
|
const list=sgs.length?sgs.map(s=>{const st=SUG_ST[s.status]||SUG_ST.new;return `<div style="border:1px solid var(--border);border-radius:10px;padding:10px 12px;margin-bottom:8px"><div style="font-size:13px">${esc(s.text)}</div><div style="margin-top:6px;display:flex;align-items:center;gap:8px;flex-wrap:wrap"><span style="font-size:11px;font-weight:700;color:${st[2]};background:${st[1]};padding:2px 8px;border-radius:6px">${st[0]}</span>${s.decision?`<span style="font-size:11px;color:#6B7280">— ${esc(s.decision)}</span>`:''}</div></div>`}).join(''):'<div style="font-size:13px;color:#9ca3af;text-align:center;padding:10px">Пока нет. Предложите первую идею — мы рассмотрим её.</div>';
|
||||||
@ -522,7 +527,7 @@ function toggleAsk(){document.getElementById("askDock").classList.toggle("open")
|
|||||||
function addAsk(role,text,dev){
|
function addAsk(role,text,dev){
|
||||||
const t=document.getElementById("askThread");if(!t)return;
|
const t=document.getElementById("askThread");if(!t)return;
|
||||||
const m=document.createElement("div");m.className="am "+(role==="user"?"u":"e");
|
const m=document.createElement("div");m.className="am "+(role==="user"?"u":"e");
|
||||||
m.innerHTML=`<div class="am-av">${role==="user"?"Я":"Е"}</div><div class="am-bb">${fmt(text)}${dev?'<div class="am-dev">⚠ Ваше пожелание зафиксировано — учтём при внедрении</div>':''}</div>`;
|
m.innerHTML=`<div class="am-av">${role==="user"?"Я":"Е"}</div><div class="am-bb">${fmt(text)}${dev?'<div class="am-dev">'+ic('alert',12)+' Ваше пожелание зафиксировано — учтём при внедрении</div>':''}</div>`;
|
||||||
t.appendChild(m);t.scrollTop=t.scrollHeight;
|
t.appendChild(m);t.scrollTop=t.scrollHeight;
|
||||||
}
|
}
|
||||||
function renderAskThread(){
|
function renderAskThread(){
|
||||||
@ -532,14 +537,14 @@ function renderAskThread(){
|
|||||||
async function askAttach(files){
|
async function askAttach(files){
|
||||||
document.getElementById("askDock").classList.add("open");
|
document.getElementById("askDock").classList.add("open");
|
||||||
for(const f of files){
|
for(const f of files){
|
||||||
addAsk("user","📎 "+f.name);
|
addAsk("user","Файл: "+f.name);
|
||||||
try{
|
try{
|
||||||
const b64=await new Promise((res,rej)=>{const r=new FileReader();r.onload=()=>res(r.result);r.onerror=rej;r.readAsDataURL(f)});
|
const b64=await new Promise((res,rej)=>{const r=new FileReader();r.onload=()=>res(r.result);r.onerror=rej;r.readAsDataURL(f)});
|
||||||
const r=await fetch(`${API}/api/upload`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,filename:f.name,content:b64})});
|
const r=await fetch(`${API}/api/upload`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,filename:f.name,content:b64})});
|
||||||
const d=await r.json();
|
const d=await r.json();
|
||||||
if(d.error){addAsk("elena","Не удалось загрузить «"+f.name+"»: "+d.error);continue;}
|
if(d.error){addAsk("elena","Не удалось загрузить «"+f.name+"»: "+d.error);continue;}
|
||||||
state.documents=state.documents||[];state.documents.push({filename:d.filename,size:d.size});
|
state.documents=state.documents||[];state.documents.push({filename:d.filename,size:d.size});
|
||||||
addAsk("elena","📎 «"+f.name+"» приложен — учту его в ответах по проекту. Спрашивайте.");
|
addAsk("elena","Документ «"+f.name+"» приложен — учту его в ответах по проекту. Спрашивайте.");
|
||||||
}catch(e){addAsk("elena","Ошибка загрузки: "+e.message);}
|
}catch(e){addAsk("elena","Ошибка загрузки: "+e.message);}
|
||||||
}
|
}
|
||||||
document.getElementById("askFile").value="";
|
document.getElementById("askFile").value="";
|
||||||
@ -598,9 +603,9 @@ async function handleFiles(files){
|
|||||||
const b64=await new Promise((res,rej)=>{const r=new FileReader();r.onload=()=>res(r.result);r.onerror=rej;r.readAsDataURL(f)});
|
const b64=await new Promise((res,rej)=>{const r=new FileReader();r.onload=()=>res(r.result);r.onerror=rej;r.readAsDataURL(f)});
|
||||||
const r=await fetch(`${API}/api/upload`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,filename:f.name,content:b64})});
|
const r=await fetch(`${API}/api/upload`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,filename:f.name,content:b64})});
|
||||||
const d=await r.json();
|
const d=await r.json();
|
||||||
if(d.error){tmp.innerHTML=`❌ ${esc(f.name)}: ${d.error}`;continue}
|
if(d.error){tmp.innerHTML=`${ic('alert',13)} ${esc(f.name)}: ${d.error}`;continue}
|
||||||
state.documents=state.documents||[];state.documents.push({filename:d.filename,size:d.size});
|
state.documents=state.documents||[];state.documents.push({filename:d.filename,size:d.size});
|
||||||
}catch(e){tmp.innerHTML=`❌ ${esc(f.name)}: ${e.message}`;continue}
|
}catch(e){tmp.innerHTML=`${ic('alert',13)} ${esc(f.name)}: ${e.message}`;continue}
|
||||||
}
|
}
|
||||||
renderDocs();
|
renderDocs();
|
||||||
}
|
}
|
||||||
@ -657,7 +662,7 @@ async function signConfirm(){
|
|||||||
state.signed=true;
|
state.signed=true;
|
||||||
document.getElementById("signModal").classList.remove("show");
|
document.getElementById("signModal").classList.remove("show");
|
||||||
document.getElementById("signStep1").style.display="block";document.getElementById("signStep2").style.display="none";
|
document.getElementById("signStep1").style.display="block";document.getElementById("signStep2").style.display="none";
|
||||||
alert("✓ Договор подписан\nПодписант: "+d.identifier);
|
alert("Договор подписан.\nПодписант: "+d.identifier);
|
||||||
document.getElementById("payModal").classList.add("show"); // сразу к оплате
|
document.getElementById("payModal").classList.add("show"); // сразу к оплате
|
||||||
}catch(e){alert("Ошибка: "+e.message)}
|
}catch(e){alert("Ошибка: "+e.message)}
|
||||||
}
|
}
|
||||||
@ -668,7 +673,7 @@ async function payVia(method){
|
|||||||
const r=await fetch(`${API}/api/payment/create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,amount,method,return_url:location.href})});
|
const r=await fetch(`${API}/api/payment/create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,amount,method,return_url:location.href})});
|
||||||
const d=await r.json();
|
const d=await r.json();
|
||||||
if(d.error){alert("Ошибка: "+d.error);return}
|
if(d.error){alert("Ошибка: "+d.error);return}
|
||||||
if(method==="cash"){alert("💵 "+d.instructions);return}
|
if(method==="cash"){alert(d.instructions);return}
|
||||||
if(d.demo){alert("ДЕМО-режим: ЮKassa ещё не подключена.\n\n"+d.note);return}
|
if(d.demo){alert("ДЕМО-режим: ЮKassa ещё не подключена.\n\n"+d.note);return}
|
||||||
if(d.qr){alert("СБП: отсканируйте QR в приложении банка\n\n"+(d.qr||""));return}
|
if(d.qr){alert("СБП: отсканируйте QR в приложении банка\n\n"+(d.qr||""));return}
|
||||||
if(d.confirmation_url){location.href=d.confirmation_url;return}
|
if(d.confirmation_url){location.href=d.confirmation_url;return}
|
||||||
@ -733,7 +738,7 @@ function renderOrgChart(o){
|
|||||||
${u.reports_to&&u.reports_to!=='—'?`<span style="font-size:11px;color:#9ca3af">↑ ${esc(u.reports_to)}</span>`:'<span style="font-size:10px;font-weight:700;color:#6366F1;background:#EEF2FF;padding:1px 8px;border-radius:5px">руководство</span>'}
|
${u.reports_to&&u.reports_to!=='—'?`<span style="font-size:11px;color:#9ca3af">↑ ${esc(u.reports_to)}</span>`:'<span style="font-size:10px;font-weight:700;color:#6366F1;background:#EEF2FF;padding:1px 8px;border-radius:5px">руководство</span>'}
|
||||||
</div>
|
</div>
|
||||||
${(u.owns_functions&&u.owns_functions.length)?`<div style="font-size:11px;color:#6b7280;margin-top:6px">Отвечает: ${u.owns_functions.map(f=>`<span style="background:#F3F4F6;padding:1px 6px;border-radius:4px;margin-right:3px;display:inline-block">${esc(f)}</span>`).join('')}</div>`:''}
|
${(u.owns_functions&&u.owns_functions.length)?`<div style="font-size:11px;color:#6b7280;margin-top:6px">Отвечает: ${u.owns_functions.map(f=>`<span style="background:#F3F4F6;padding:1px 6px;border-radius:4px;margin-right:3px;display:inline-block">${esc(f)}</span>`).join('')}</div>`:''}
|
||||||
${u.note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:6px">⚠ ${esc(u.note)}</div>`:''}
|
${u.note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:6px">${ic('alert',12)} ${esc(u.note)}</div>`:''}
|
||||||
</div>`).join('');
|
</div>`).join('');
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
@ -745,7 +750,7 @@ function renderJobs(j){
|
|||||||
${(r.responsibilities&&r.responsibilities.length)?`<div style="font-size:11px;font-weight:700;color:#374151;margin-bottom:3px">Зоны ответственности</div><ul style="margin:0 0 9px;padding-left:18px;font-size:12px;color:#4B5563">${r.responsibilities.map(x=>`<li>${esc(x)}</li>`).join('')}</ul>`:''}
|
${(r.responsibilities&&r.responsibilities.length)?`<div style="font-size:11px;font-weight:700;color:#374151;margin-bottom:3px">Зоны ответственности</div><ul style="margin:0 0 9px;padding-left:18px;font-size:12px;color:#4B5563">${r.responsibilities.map(x=>`<li>${esc(x)}</li>`).join('')}</ul>`:''}
|
||||||
${(r.kpis&&r.kpis.length)?`<div style="font-size:11px;font-weight:700;color:#047857;margin-bottom:3px">KPI</div><div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:9px">${r.kpis.map(k=>`<span style="font-size:11px;background:#ECFDF5;color:#047857;padding:2px 8px;border-radius:5px">${esc(k)}</span>`).join('')}</div>`:''}
|
${(r.kpis&&r.kpis.length)?`<div style="font-size:11px;font-weight:700;color:#047857;margin-bottom:3px">KPI</div><div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:9px">${r.kpis.map(k=>`<span style="font-size:11px;background:#ECFDF5;color:#047857;padding:2px 8px;border-radius:5px">${esc(k)}</span>`).join('')}</div>`:''}
|
||||||
${(r.authority&&r.authority.length)?`<div style="font-size:11px;color:#6b7280">Полномочия: ${r.authority.map(a=>esc(a)).join(' · ')}</div>`:''}
|
${(r.authority&&r.authority.length)?`<div style="font-size:11px;color:#6b7280">Полномочия: ${r.authority.map(a=>esc(a)).join(' · ')}</div>`:''}
|
||||||
${r.deviation_note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:8px">⚠ Учтено пожелание клиента: ${esc(r.deviation_note)}</div>`:''}
|
${r.deviation_note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:8px">${ic('alert',12)} Учтено пожелание клиента: ${esc(r.deviation_note)}</div>`:''}
|
||||||
</div>`).join('');
|
</div>`).join('');
|
||||||
}
|
}
|
||||||
function renderSpecPane(){
|
function renderSpecPane(){
|
||||||
@ -789,10 +794,10 @@ function renderEstimateCard(){
|
|||||||
const dd=due[s.key]||'';
|
const dd=due[s.key]||'';
|
||||||
let tag='';
|
let tag='';
|
||||||
if(isFree)tag='<span style="font-size:10px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 7px;border-radius:5px">бесплатно</span>';
|
if(isFree)tag='<span style="font-size:10px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 7px;border-radius:5px">бесплатно</span>';
|
||||||
else if(isPaid)tag='<span style="font-size:10px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 7px;border-radius:5px">✓ оплачено</span>';
|
else if(isPaid)tag='<span style="font-size:10px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 7px;border-radius:5px">'+ic('check',11)+' оплачено</span>';
|
||||||
else if(done)tag='<span style="font-size:10px;font-weight:700;color:#92400E;background:#FEF3C7;padding:1px 7px;border-radius:5px">к оплате</span>';
|
else if(done)tag='<span style="font-size:10px;font-weight:700;color:#92400E;background:#FEF3C7;padding:1px 7px;border-radius:5px">к оплате</span>';
|
||||||
else tag='<span style="font-size:10px;font-weight:700;color:#9CA3AF;background:#F3F4F6;padding:1px 7px;border-radius:5px">в работе</span>';
|
else tag='<span style="font-size:10px;font-weight:700;color:#9CA3AF;background:#F3F4F6;padding:1px 7px;border-radius:5px">в работе</span>';
|
||||||
const dueChip=(dd&&!isPaid&&!isFree)?`<span style="font-size:10px;font-weight:600;color:#2563EB;background:#EFF6FF;padding:1px 7px;border-radius:5px">📅 до ${fmtD(dd)}</span>`:'';
|
const dueChip=(dd&&!isPaid&&!isFree)?`<span style="font-size:10px;font-weight:600;color:#2563EB;background:#EFF6FF;padding:1px 7px;border-radius:5px">${ic('calendar',11)} до ${fmtD(dd)}</span>`:'';
|
||||||
return `<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-top:1px solid rgba(0,0,0,.06)">
|
return `<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-top:1px solid rgba(0,0,0,.06)">
|
||||||
<span style="font-size:15px">${s.icon}</span>
|
<span style="font-size:15px">${s.icon}</span>
|
||||||
<div style="flex:1;min-width:0"><div style="font-size:13px;font-weight:700;display:flex;align-items:center;gap:7px;flex-wrap:wrap">${s.name} ${tag} ${dueChip}</div><div style="font-size:11px;color:#6b7280;margin-top:1px">${s.desc}</div></div>
|
<div style="flex:1;min-width:0"><div style="font-size:13px;font-weight:700;display:flex;align-items:center;gap:7px;flex-wrap:wrap">${s.name} ${tag} ${dueChip}</div><div style="font-size:11px;color:#6b7280;margin-top:1px">${s.desc}</div></div>
|
||||||
@ -808,7 +813,7 @@ function renderEstimateCard(){
|
|||||||
}
|
}
|
||||||
function renderExportBar(){
|
function renderExportBar(){
|
||||||
if(state.unlocked){
|
if(state.unlocked){
|
||||||
return renderEstimateCard()+`<div class="exp-bar"><div class="exp-h">📦 Готовые документы</div>
|
return renderEstimateCard()+`<div class="exp-bar"><div class="exp-h" style="display:flex;align-items:center;gap:7px">${ic('crm',17)} Готовые документы</div>
|
||||||
<div class="exp-d">Долг закрыт — документы и ТЗ доступны для печати и выгрузки.</div>
|
<div class="exp-d">Долг закрыт — документы и ТЗ доступны для печати и выгрузки.</div>
|
||||||
<div class="exp-btns">
|
<div class="exp-btns">
|
||||||
<button class="btn btn-p" onclick="printDoc()">${ic('printer',16)} Печать / PDF</button>
|
<button class="btn btn-p" onclick="printDoc()">${ic('printer',16)} Печать / PDF</button>
|
||||||
@ -829,8 +834,8 @@ function buildTZmd(s){
|
|||||||
(s.roles||[]).forEach(r=>m+=`- **${r.name}** — ${r.does} (доступ: ${r.access})\n`);
|
(s.roles||[]).forEach(r=>m+=`- **${r.name}** — ${r.does} (доступ: ${r.access})\n`);
|
||||||
m+=`\n## B. Модули\n`;(s.modules||[]).forEach(x=>{m+=`### ${x.name} [${x.source_node}]\n${x.purpose}\nЭкраны: ${(x.screens||[]).join(', ')}\nДанные: вход — ${x.inputs_data}; выход — ${x.outputs_data}\n`;(x.rules||[]).forEach(r=>m+=`- правило: ${r}\n`);m+=`\n`});
|
m+=`\n## B. Модули\n`;(s.modules||[]).forEach(x=>{m+=`### ${x.name} [${x.source_node}]\n${x.purpose}\nЭкраны: ${(x.screens||[]).join(', ')}\nДанные: вход — ${x.inputs_data}; выход — ${x.outputs_data}\n`;(x.rules||[]).forEach(r=>m+=`- правило: ${r}\n`);m+=`\n`});
|
||||||
m+=`## C. Модель данных\n`;(s.entities||[]).forEach(e=>{m+=`### ${e.name}\n`;(e.fields||[]).forEach(f=>m+=`- ${f.field}: ${f.type}\n`);if((e.relations||[]).length)m+=`Связи: ${e.relations.join(' · ')}\n`;m+=`Пример: ${e.example}\n\n`});
|
m+=`## C. Модель данных\n`;(s.entities||[]).forEach(e=>{m+=`### ${e.name}\n`;(e.fields||[]).forEach(f=>m+=`- ${f.field}: ${f.type}\n`);if((e.relations||[]).length)m+=`Связи: ${e.relations.join(' · ')}\n`;m+=`Пример: ${e.example}\n\n`});
|
||||||
if(state.orgchart&&(state.orgchart.units||[]).length){m+=`\n## D. Оргструктура\n${state.orgchart.insight||''}\n`;state.orgchart.units.forEach(u=>{m+=`- **${u.role}** (${(+u.headcount||1)} чел.)${u.reports_to&&u.reports_to!=='—'?` ↑ ${u.reports_to}`:''}${(u.owns_functions||[]).length?` — отвечает: ${u.owns_functions.join(', ')}`:''}${u.note?` ⚠ ${u.note}`:''}\n`})}
|
if(state.orgchart&&(state.orgchart.units||[]).length){m+=`\n## D. Оргструктура\n${state.orgchart.insight||''}\n`;state.orgchart.units.forEach(u=>{m+=`- **${u.role}** (${(+u.headcount||1)} чел.)${u.reports_to&&u.reports_to!=='—'?` ↑ ${u.reports_to}`:''}${(u.owns_functions||[]).length?` — отвечает: ${u.owns_functions.join(', ')}`:''}${u.note?` (внимание: ${u.note})`:''}\n`})}
|
||||||
if(state.jobs&&(state.jobs.roles||[]).length){m+=`\n## E. Должностные инструкции\n`;state.jobs.roles.forEach(r=>{m+=`### ${r.role}\n${r.purpose||''}${r.reports_to?` (подчинение: ${r.reports_to})`:''}\n`;(r.responsibilities||[]).forEach(x=>m+=`- ${x}\n`);if((r.kpis||[]).length)m+=`KPI: ${r.kpis.join(' · ')}\n`;if((r.authority||[]).length)m+=`Полномочия: ${r.authority.join(' · ')}\n`;if(r.deviation_note)m+=`⚠ Учтено пожелание клиента: ${r.deviation_note}\n`;m+=`\n`})}
|
if(state.jobs&&(state.jobs.roles||[]).length){m+=`\n## E. Должностные инструкции\n`;state.jobs.roles.forEach(r=>{m+=`### ${r.role}\n${r.purpose||''}${r.reports_to?` (подчинение: ${r.reports_to})`:''}\n`;(r.responsibilities||[]).forEach(x=>m+=`- ${x}\n`);if((r.kpis||[]).length)m+=`KPI: ${r.kpis.join(' · ')}\n`;if((r.authority||[]).length)m+=`Полномочия: ${r.authority.join(' · ')}\n`;if(r.deviation_note)m+=`Внимание — учтено пожелание клиента: ${r.deviation_note}\n`;m+=`\n`})}
|
||||||
if((s.open_questions||[]).length){m+=`## Уточнить перед разработкой\n`;s.open_questions.forEach(q=>m+=`- ${q}\n`)}
|
if((s.open_questions||[]).length){m+=`## Уточнить перед разработкой\n`;s.open_questions.forEach(q=>m+=`- ${q}\n`)}
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
@ -864,8 +869,8 @@ function renderIdef(m){const box=(fn,ct,ins,outs,me,id,pct)=>{const C=(ct&&ct.le
|
|||||||
return h}
|
return h}
|
||||||
function renderSpec(s){let h=`<div class="spec-h"><span class="pl">A</span>Обзор</div><div class="blk">${esc(s.overview)}</div>`;
|
function renderSpec(s){let h=`<div class="spec-h"><span class="pl">A</span>Обзор</div><div class="blk">${esc(s.overview)}</div>`;
|
||||||
h+=`<div class="spec-h"><span class="pl">A</span>Роли (${s.roles.length})</div>`;s.roles.forEach(r=>h+=`<div class="blk" style="padding:12px 15px"><b>${esc(r.name)}</b> — ${esc(r.does)}<div style="font-size:11px;color:#9ca3af;margin-top:3px">Доступ: ${esc(r.access)}</div></div>`);
|
h+=`<div class="spec-h"><span class="pl">A</span>Роли (${s.roles.length})</div>`;s.roles.forEach(r=>h+=`<div class="blk" style="padding:12px 15px"><b>${esc(r.name)}</b> — ${esc(r.does)}<div style="font-size:11px;color:#9ca3af;margin-top:3px">Доступ: ${esc(r.access)}</div></div>`);
|
||||||
h+=`<div class="spec-h"><span class="pl">B</span>Модули (${s.modules.length})</div>`;s.modules.forEach(m=>h+=`<div class="mod"><div class="mod-top"><span class="mod-node">${esc(m.source_node)}</span><span class="mod-name">${esc(m.name)}</span></div><div style="font-size:12px;color:var(--muted)">${esc(m.purpose)}</div><div style="margin:6px 0">${m.screens.map(s=>`<span class="scr">${esc(s)}</span>`).join("")}</div><div class="mod-data">📥 ${esc(m.inputs_data)} · 📤 ${esc(m.outputs_data)}</div>${m.rules.length?`<div style="margin-top:6px">${m.rules.map(r=>`<div class="blk-pain">${esc(r)}</div>`).join("")}</div>`:''}</div>`);
|
h+=`<div class="spec-h"><span class="pl">B</span>Модули (${s.modules.length})</div>`;s.modules.forEach(m=>h+=`<div class="mod"><div class="mod-top"><span class="mod-node">${esc(m.source_node)}</span><span class="mod-name">${esc(m.name)}</span></div><div style="font-size:12px;color:var(--muted)">${esc(m.purpose)}</div><div style="margin:6px 0">${m.screens.map(s=>`<span class="scr">${esc(s)}</span>`).join("")}</div><div class="mod-data">Вход: ${esc(m.inputs_data)} · Выход: ${esc(m.outputs_data)}</div>${m.rules.length?`<div style="margin-top:6px">${m.rules.map(r=>`<div class="blk-pain">${esc(r)}</div>`).join("")}</div>`:''}</div>`);
|
||||||
h+=`<div class="spec-h"><span class="pl">C</span>Модель данных (${s.entities.length} таблиц)</div>`;s.entities.forEach(e=>h+=`<div class="ent"><div class="ent-name">◆ ${esc(e.name)}</div><div class="ent-fields">${e.fields.map(f=>`<div class="fld"><b>${esc(f.field)}</b> <em>${esc(f.type)}</em></div>`).join("")}</div>${e.relations.length?`<div style="font-size:11px;color:#6b7280;margin-bottom:6px">🔗 ${e.relations.map(esc).join(" · ")}</div>`:''}<div class="ent-ex">${esc(e.example)}</div></div>`);
|
h+=`<div class="spec-h"><span class="pl">C</span>Модель данных (${s.entities.length} таблиц)</div>`;s.entities.forEach(e=>h+=`<div class="ent"><div class="ent-name">◆ ${esc(e.name)}</div><div class="ent-fields">${e.fields.map(f=>`<div class="fld"><b>${esc(f.field)}</b> <em>${esc(f.type)}</em></div>`).join("")}</div>${e.relations.length?`<div style="font-size:11px;color:#6b7280;margin-bottom:6px">Связи: ${e.relations.map(esc).join(" · ")}</div>`:''}<div class="ent-ex">${esc(e.example)}</div></div>`);
|
||||||
if(s.open_questions&&s.open_questions.length){h+=`<div class="blk" style="border-color:#FDE68A;background:#FFFBEB"><div style="font-weight:700;margin-bottom:8px">Уточнить перед разработкой</div>${s.open_questions.map(q=>`<div class="blk-pain">${esc(q)}</div>`).join("")}</div>`}
|
if(s.open_questions&&s.open_questions.length){h+=`<div class="blk" style="border-color:#FDE68A;background:#FFFBEB"><div style="font-weight:700;margin-bottom:8px">Уточнить перед разработкой</div>${s.open_questions.map(q=>`<div class="blk-pain">${esc(q)}</div>`).join("")}</div>`}
|
||||||
return h}
|
return h}
|
||||||
|
|
||||||
|
|||||||
116
docs/crm.html
116
docs/crm.html
@ -163,7 +163,7 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="hdr"><button class="hdr-burger" onclick="toggleSb()" aria-label="Меню">☰</button><div class="hdr-ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></div><div class="hdr-t">@wasrusgen1<span class="hdr-sep"></span><b>КОНСАЛТИНГ</b></div><div class="hdr-badge">CRM</div><div class="hdr-r"><span id="aiDot" title="AI-движок Елены" style="width:8px;height:8px;border-radius:50%;background:#9CA3AF"></span>Руслан</div></header>
|
<header class="hdr"><button class="hdr-burger" onclick="toggleSb()" aria-label="Меню">☰</button><div class="hdr-ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></div><div class="hdr-t">@wasrusgen1<span class="hdr-sep"></span><b>КОНСАЛТИНГ</b></div><div class="hdr-badge">CRM</div><div class="hdr-r"><span id="aiDot" title="AI-движок Елены" style="width:8px;height:8px;border-radius:50%;background:#9CA3AF"></span>Руслан</div></header>
|
||||||
<div id="aiBanner" style="display:none;align-items:center;gap:10px;background:#FEF2F2;border-bottom:1px solid #FCA5A5;color:#991B1B;padding:9px 18px;font-size:13px;font-weight:600"><span id="aiBannerText">AI-движок недоступен</span></div>
|
<div id="aiBanner" style="display:none;align-items:center;gap:8px;background:#FEF2F2;border-bottom:1px solid #FCA5A5;color:#991B1B;padding:9px 18px;font-size:13px;font-weight:600"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg><span id="aiBannerText">AI-движок недоступен</span></div>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<div class="sb-backdrop" id="sbBackdrop" onclick="toggleSb()"></div>
|
<div class="sb-backdrop" id="sbBackdrop" onclick="toggleSb()"></div>
|
||||||
<aside class="sb" id="sbNav">
|
<aside class="sb" id="sbNav">
|
||||||
@ -212,7 +212,19 @@ const ICONS={
|
|||||||
dashboard:'<rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/>',
|
dashboard:'<rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/>',
|
||||||
receipt:'<path d="M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1-2-1z"/><path d="M16 8H8M16 12H8M13 16H8"/>',
|
receipt:'<path d="M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1-2-1z"/><path d="M16 8H8M16 12H8M13 16H8"/>',
|
||||||
screen:'<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>',
|
screen:'<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>',
|
||||||
send:'<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>'
|
send:'<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>',
|
||||||
|
close:'<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
||||||
|
check:'<polyline points="20 6 9 17 4 12"/>',
|
||||||
|
checkCircle:'<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
|
||||||
|
gift:'<polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/>',
|
||||||
|
dot:'<circle cx="12" cy="12" r="5"/>',
|
||||||
|
edit:'<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>',
|
||||||
|
link:'<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
|
||||||
|
eye:'<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>',
|
||||||
|
trash:'<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>',
|
||||||
|
wrench:'<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
|
||||||
|
ruler:'<path d="M16 3l5 5L8 21l-5-5L16 3z"/><path d="M14 5l1.5 1.5M11 8l1.5 1.5M8 11l1.5 1.5M5 14l1.5 1.5"/>',
|
||||||
|
calendar:'<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>'
|
||||||
};
|
};
|
||||||
function ic(n,s,sw){s=s||20;return '<svg style="display:inline-block;vertical-align:middle" width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="'+(sw||1.75)+'" stroke-linecap="round" stroke-linejoin="round">'+(ICONS[n]||'')+'</svg>';}
|
function ic(n,s,sw){s=s||20;return '<svg style="display:inline-block;vertical-align:middle" width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="'+(sw||1.75)+'" stroke-linecap="round" stroke-linejoin="round">'+(ICONS[n]||'')+'</svg>';}
|
||||||
// ── Мобильное меню ──
|
// ── Мобильное меню ──
|
||||||
@ -286,7 +298,7 @@ async function opAuth(body){
|
|||||||
}catch(e){return {ok:false,error:e.message};}
|
}catch(e){return {ok:false,error:e.message};}
|
||||||
}
|
}
|
||||||
function showDenied(msg){
|
function showDenied(msg){
|
||||||
document.body.innerHTML=`<div style="height:100vh;display:flex;align-items:center;justify-content:center;background:#0F0F1A;color:#fff;font-family:Inter;text-align:center;padding:24px"><div><div style="font-size:40px;margin-bottom:12px">🔒</div><div style="font-size:18px;font-weight:800;font-family:Montserrat">Доступ только для оператора</div><div style="font-size:13px;color:#8A94A6;margin-top:8px;max-width:340px">${msg}</div></div></div>`;
|
document.body.innerHTML=`<div style="height:100vh;display:flex;align-items:center;justify-content:center;background:#0F0F1A;color:#fff;font-family:Inter;text-align:center;padding:24px"><div><div style="margin-bottom:12px;display:flex;justify-content:center;color:#fff">${ic('lock',40)}</div><div style="font-size:18px;font-weight:800;font-family:Montserrat">Доступ только для оператора</div><div style="font-size:13px;color:#8A94A6;margin-top:8px;max-width:340px">${msg}</div></div></div>`;
|
||||||
}
|
}
|
||||||
function showOpLogin(){
|
function showOpLogin(){
|
||||||
document.body.innerHTML=`<div id="opLogin" style="height:100vh;display:flex;align-items:center;justify-content:center;background:#0F0F1A;font-family:Inter;padding:24px">
|
document.body.innerHTML=`<div id="opLogin" style="height:100vh;display:flex;align-items:center;justify-content:center;background:#0F0F1A;font-family:Inter;padding:24px">
|
||||||
@ -451,8 +463,8 @@ function renderClientRow(p){
|
|||||||
const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];
|
const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];
|
||||||
const billing=(p.crm&&p.crm.billing_type)||"paid";
|
const billing=(p.crm&&p.crm.billing_type)||"paid";
|
||||||
const bChip=billing==="free"
|
const bChip=billing==="free"
|
||||||
?`<span class="mini-chip" style="color:#6366F1;background:#EEF2FF">🎁 Беспл.</span>`
|
?`<span class="mini-chip" style="color:#6366F1;background:#EEF2FF">${ic('gift',12)} Беспл.</span>`
|
||||||
:`<span class="mini-chip" style="color:#047857;background:#ECFDF5">💰 Платный</span>`;
|
:`<span class="mini-chip" style="color:#047857;background:#ECFDF5">${ic('wallet',12)} Платный</span>`;
|
||||||
return `<div class="cl-row" onclick="openClient('${p.token}')">
|
return `<div class="cl-row" onclick="openClient('${p.token}')">
|
||||||
<div class="cl-av" style="width:32px;height:32px;border-radius:8px;font-size:13px;flex-shrink:0">${esc((p.client_name||'?')[0])}</div>
|
<div class="cl-av" style="width:32px;height:32px;border-radius:8px;font-size:13px;flex-shrink:0">${esc((p.client_name||'?')[0])}</div>
|
||||||
<div style="flex:1;min-width:0">
|
<div style="flex:1;min-width:0">
|
||||||
@ -510,7 +522,7 @@ function renderPipeline(){
|
|||||||
}
|
}
|
||||||
function kCard(p){
|
function kCard(p){
|
||||||
const billing=(p.crm&&p.crm.billing_type)||"paid";
|
const billing=(p.crm&&p.crm.billing_type)||"paid";
|
||||||
const bch=billing==='free'?'<span class="mini-chip" style="color:#6366F1;background:#EEF2FF">🎁</span>':'<span class="mini-chip" style="color:#047857;background:#ECFDF5">💰</span>';
|
const bch=billing==='free'?'<span class="mini-chip" style="color:#6366F1;background:#EEF2FF">'+ic('gift',12)+'</span>':'<span class="mini-chip" style="color:#047857;background:#ECFDF5">'+ic('wallet',12)+'</span>';
|
||||||
const st=clientStages(p);
|
const st=clientStages(p);
|
||||||
let dots="";STAGE_DEFS.forEach((s,i)=>{const d=st.done[i],c=!st.all&&i===st.cur;dots+=`<span title="${s.name}" style="width:7px;height:7px;border-radius:50%;background:${d?'#047857':c?'#10B981':'#E5E7EB'};display:inline-block"></span>`;});
|
let dots="";STAGE_DEFS.forEach((s,i)=>{const d=st.done[i],c=!st.all&&i===st.cur;dots+=`<span title="${s.name}" style="width:7px;height:7px;border-radius:50%;background:${d?'#047857':c?'#10B981':'#E5E7EB'};display:inline-block"></span>`;});
|
||||||
return `<div class="kcard" draggable="true" ondragstart="kDragStart(event,'${p.token}')" ondragend="kDragEnd(event)" onclick="openClient('${p.token}')">
|
return `<div class="kcard" draggable="true" ondragstart="kDragStart(event,'${p.token}')" ondragend="kDragEnd(event)" onclick="openClient('${p.token}')">
|
||||||
@ -550,8 +562,8 @@ function renderClient(){
|
|||||||
document.getElementById("view").innerHTML=`
|
document.getElementById("view").innerHTML=`
|
||||||
<div class="cc-top"><div class="cc-av">${esc((state.client_name||'?')[0])}</div><div style="flex:1"><div class="cc-name">${esc(state.client_name||'Без имени')}</div><div class="cc-meta">${esc(state.niche||'')} · ${state.messages.length} сообщений</div></div>
|
<div class="cc-top"><div class="cc-av">${esc((state.client_name||'?')[0])}</div><div style="flex:1"><div class="cc-name">${esc(state.client_name||'Без имени')}</div><div class="cc-meta">${esc(state.niche||'')} · ${state.messages.length} сообщений</div></div>
|
||||||
<div style="display:flex;gap:4px;background:#F1F5F9;border-radius:10px;padding:3px">
|
<div style="display:flex;gap:4px;background:#F1F5F9;border-radius:10px;padding:3px">
|
||||||
<button onclick="setBilling('paid')" style="padding:7px 14px;border-radius:8px;border:none;cursor:pointer;font-size:13px;font-weight:700;font-family:Inter;${billing==='paid'?'background:#047857;color:#fff':'background:transparent;color:#6B7280'}">💰 Платный</button>
|
<button onclick="setBilling('paid')" style="padding:7px 14px;border-radius:8px;border:none;cursor:pointer;font-size:13px;font-weight:700;font-family:Inter;${billing==='paid'?'background:#047857;color:#fff':'background:transparent;color:#6B7280'}">${ic('wallet',15)} Платный</button>
|
||||||
<button onclick="setBilling('free')" style="padding:7px 14px;border-radius:8px;border:none;cursor:pointer;font-size:13px;font-weight:700;font-family:Inter;${billing==='free'?'background:#6366F1;color:#fff':'background:transparent;color:#6B7280'}">🎁 Бесплатный</button>
|
<button onclick="setBilling('free')" style="padding:7px 14px;border-radius:8px;border:none;cursor:pointer;font-size:13px;font-weight:700;font-family:Inter;${billing==='free'?'background:#6366F1;color:#fff':'background:transparent;color:#6B7280'}">${ic('gift',15)} Бесплатный</button>
|
||||||
</div></div>
|
</div></div>
|
||||||
<div class="mtabs">${MAINTABS.map(t=>`<button class="mtab ${t.id===mainTab?'active':''}" onclick="setMainTab('${t.id}')">${t.icon} ${t.name}${badge(t.id)}</button>`).join("")}</div>
|
<div class="mtabs">${MAINTABS.map(t=>`<button class="mtab ${t.id===mainTab?'active':''}" onclick="setMainTab('${t.id}')">${t.icon} ${t.name}${badge(t.id)}</button>`).join("")}</div>
|
||||||
<div id="mainPanel"></div>`;
|
<div id="mainPanel"></div>`;
|
||||||
@ -588,13 +600,13 @@ function opBubble(m){const me=m.role==='assistant';return `<div style="display:f
|
|||||||
function renderOpChatTab(){
|
function renderOpChatTab(){
|
||||||
const box=document.getElementById("opChatBox");if(!box)return;
|
const box=document.getElementById("opChatBox");if(!box)return;
|
||||||
const msgs=state.operator_chat||[];
|
const msgs=state.operator_chat||[];
|
||||||
box.innerHTML=`<div style="font-size:12px;color:#6B7280;margin-bottom:10px">Живой канал с клиентом. Он видит это в кабинете → «💬 Консультант» и получает уведомление в Telegram.</div>
|
box.innerHTML=`<div style="font-size:12px;color:#6B7280;margin-bottom:10px">Живой канал с клиентом. Он видит это в кабинете → «Консультант» и получает уведомление в Telegram.</div>
|
||||||
<div style="background:#fff;border:1.5px solid var(--border);border-radius:12px;overflow:hidden">
|
<div style="background:#fff;border:1.5px solid var(--border);border-radius:12px;overflow:hidden">
|
||||||
<div id="opThread" style="max-height:400px;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px">
|
<div id="opThread" style="max-height:400px;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px">
|
||||||
${msgs.length?msgs.map(opBubble).join(''):'<div style="text-align:center;color:#cbd5e1;font-size:13px;padding:16px">Сообщений нет. Напишите клиенту первым.</div>'}
|
${msgs.length?msgs.map(opBubble).join(''):'<div style="text-align:center;color:#cbd5e1;font-size:13px;padding:16px">Сообщений нет. Напишите клиенту первым.</div>'}
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:8px;padding:10px 12px;border-top:1px solid var(--border)">
|
<div style="display:flex;gap:8px;padding:10px 12px;border-top:1px solid var(--border)">
|
||||||
<textarea id="opChatInp" rows="1" placeholder="Сообщение клиенту… или 🎤" style="flex:1;border:1.5px solid var(--border);border-radius:9px;padding:9px 12px;font-size:13px;font-family:Inter;resize:none;outline:none;max-height:120px" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();opChatSend()}"></textarea>
|
<textarea id="opChatInp" rows="1" placeholder="Сообщение клиенту… или голосом" style="flex:1;border:1.5px solid var(--border);border-radius:9px;padding:9px 12px;font-size:13px;font-family:Inter;resize:none;outline:none;max-height:120px" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();opChatSend()}"></textarea>
|
||||||
${micBtn('opChatInp',36)}
|
${micBtn('opChatInp',36)}
|
||||||
<button class="cp-btn cp-a" onclick="opChatSend()">Отправить</button>
|
<button class="cp-btn cp-a" onclick="opChatSend()">Отправить</button>
|
||||||
</div></div>`;
|
</div></div>`;
|
||||||
@ -620,11 +632,11 @@ async function crmChatPollOnce(){
|
|||||||
}
|
}
|
||||||
function startCrmChatPoll(){stopCrmChatPoll();crmChatPoll=setInterval(crmChatPollOnce,15000);}
|
function startCrmChatPoll(){stopCrmChatPoll();crmChatPoll=setInterval(crmChatPollOnce,15000);}
|
||||||
function stopCrmChatPoll(){if(crmChatPoll){clearInterval(crmChatPoll);crmChatPoll=null;}}
|
function stopCrmChatPoll(){if(crmChatPoll){clearInterval(crmChatPoll);crmChatPoll=null;}}
|
||||||
const DEV_STAGE={canvas:"📊 Стратегия",idef0:"🔧 Функции",spec:"📋 ТЗ",documents:"📁 Документы",methods:"🎯 Методологии",interview:"💬 Интервью"};
|
const DEV_STAGE={canvas:ic('chart',13)+" Стратегия",idef0:ic('process',13)+" Функции",spec:ic('clipboard',13)+" ТЗ",documents:ic('folder',13)+" Документы",methods:ic('target',13)+" Методологии",interview:ic('message',13)+" Интервью"};
|
||||||
function renderDeviations(){
|
function renderDeviations(){
|
||||||
const dev=state.deviations||[];
|
const dev=state.deviations||[];
|
||||||
if(!dev.length)return `<div class="run-card" style="background:#fff;border:1px solid #E5E7EB;border-radius:12px;padding:22px;text-align:center">
|
if(!dev.length)return `<div class="run-card" style="background:#fff;border:1px solid #E5E7EB;border-radius:12px;padding:22px;text-align:center">
|
||||||
<div style="font-size:26px;margin-bottom:6px">✅</div>
|
<div style="color:#10B981;margin-bottom:6px;display:flex;justify-content:center">${ic('checkCircle',28)}</div>
|
||||||
<div style="font-size:14px;font-weight:700">Отклонений нет</div>
|
<div style="font-size:14px;font-weight:700">Отклонений нет</div>
|
||||||
<div style="font-size:12px;color:#6B7280;margin-top:4px">Клиент пока не настаивал на изменениях. Когда в кабинете на этапах 3–5 он скажет Елене «мне так удобно» — расхождение появится здесь: эталон против выбора клиента.</div></div>`;
|
<div style="font-size:12px;color:#6B7280;margin-top:4px">Клиент пока не настаивал на изменениях. Когда в кабинете на этапах 3–5 он скажет Елене «мне так удобно» — расхождение появится здесь: эталон против выбора клиента.</div></div>`;
|
||||||
const fmtD=d=>{if(!d)return'';const a=String(d).slice(0,10).split('-');return a.length===3?a[2]+'.'+a[1]+'.'+a[0].slice(2):'';};
|
const fmtD=d=>{if(!d)return'';const a=String(d).slice(0,10).split('-');return a.length===3?a[2]+'.'+a[1]+'.'+a[0].slice(2):'';};
|
||||||
@ -636,15 +648,15 @@ function renderDeviations(){
|
|||||||
${d.at?`<span style="margin-left:auto;font-size:10px;color:#9CA3AF">${fmtD(d.at)}</span>`:''}
|
${d.at?`<span style="margin-left:auto;font-size:10px;color:#9CA3AF">${fmtD(d.at)}</span>`:''}
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:8px;align-items:flex-start;margin-bottom:7px">
|
<div style="display:flex;gap:8px;align-items:flex-start;margin-bottom:7px">
|
||||||
<span style="font-size:13px;flex:0 0 18px">✅</span>
|
<span style="flex:0 0 18px;color:#047857">${ic('checkCircle',16)}</span>
|
||||||
<div><div style="font-size:10px;font-weight:700;color:#047857;text-transform:uppercase;letter-spacing:.3px">Решение клиента · внедряем</div><div style="font-size:13px;font-weight:600;color:#1A1A2E">${esc(d.client_choice||'')}</div></div>
|
<div><div style="font-size:10px;font-weight:700;color:#047857;text-transform:uppercase;letter-spacing:.3px">Решение клиента · внедряем</div><div style="font-size:13px;font-weight:600;color:#1A1A2E">${esc(d.client_choice||'')}</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:8px;align-items:flex-start;margin-bottom:7px">
|
<div style="display:flex;gap:8px;align-items:flex-start;margin-bottom:7px">
|
||||||
<span style="font-size:13px;flex:0 0 18px">⚠️</span>
|
<span style="flex:0 0 18px;color:#92400E">${ic('alert',16)}</span>
|
||||||
<div><div style="font-size:10px;font-weight:700;color:#92400E;text-transform:uppercase;letter-spacing:.3px">Елена рекомендовала</div><div style="font-size:13px;color:#374151">${esc(d.elena_rec||'')}</div></div>
|
<div><div style="font-size:10px;font-weight:700;color:#92400E;text-transform:uppercase;letter-spacing:.3px">Елена рекомендовала</div><div style="font-size:13px;color:#374151">${esc(d.elena_rec||'')}</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:8px;align-items:flex-start;background:#F9FAFB;border-radius:8px;padding:8px 10px;margin-top:8px">
|
<div style="display:flex;gap:8px;align-items:flex-start;background:#F9FAFB;border-radius:8px;padding:8px 10px;margin-top:8px">
|
||||||
<span style="font-size:13px;flex:0 0 18px">💬</span>
|
<span style="flex:0 0 18px;color:#6B7280">${ic('message',16)}</span>
|
||||||
<div><div style="font-size:10px;font-weight:700;color:#6B7280;text-transform:uppercase;letter-spacing:.3px">Причина клиента</div><div style="font-size:12px;color:#4B5563;font-style:italic">${esc(d.reason||'')}</div></div>
|
<div><div style="font-size:10px;font-weight:700;color:#6B7280;text-transform:uppercase;letter-spacing:.3px">Причина клиента</div><div style="font-size:12px;color:#4B5563;font-style:italic">${esc(d.reason||'')}</div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>`).join('');
|
</div>`).join('');
|
||||||
@ -665,10 +677,10 @@ function renderMainPanel(){
|
|||||||
<div class="cc-field"><div class="cc-fl">Источник</div><input class="cc-fi" id="cpSrc" value="${esc(crm.source||'')}" placeholder="откуда пришёл" onchange="saveCrm()"></div>
|
<div class="cc-field"><div class="cc-fl">Источник</div><input class="cc-fi" id="cpSrc" value="${esc(crm.source||'')}" placeholder="откуда пришёл" onchange="saveCrm()"></div>
|
||||||
<div class="cc-field" id="payStatusBox"></div>
|
<div class="cc-field" id="payStatusBox"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cc-actions"><button class="btn btn-p" onclick="inviteLink()">🔗 Ссылка клиенту</button><button class="btn btn-p" style="background:#0088cc" onclick="inviteTelegram()">✈ В Telegram</button><a class="btn btn-g" href="cabinet.html?t=${current}" target="_blank">👁 Открыть кабинет</a><button class="btn btn-g" style="margin-left:auto;border-color:#FECACA;color:#DC2626" onclick="deleteClient()">🗑 Удалить</button></div>
|
<div class="cc-actions"><button class="btn btn-p" onclick="inviteLink()">${ic('link',15)} Ссылка клиенту</button><button class="btn btn-p" style="background:#0088cc" onclick="inviteTelegram()">${ic('send',15)} В Telegram</button><a class="btn btn-g" href="cabinet.html?t=${current}" target="_blank">${ic('eye',15)} Открыть кабинет</a><button class="btn btn-g" style="margin-left:auto;border-color:#FECACA;color:#DC2626" onclick="deleteClient()">${ic('trash',15)} Удалить</button></div>
|
||||||
<div class="deal-overview" style="display:grid;grid-template-columns:1.35fr 1fr;gap:14px;align-items:start">
|
<div class="deal-overview" style="display:grid;grid-template-columns:1.35fr 1fr;gap:14px;align-items:start">
|
||||||
<div class="blk" style="margin:0">
|
<div class="blk" style="margin:0">
|
||||||
<div style="display:flex;align-items:center;margin-bottom:6px"><b style="font-size:13px">📍 Прогресс проекта</b><span style="margin-left:auto;font-size:11px;color:var(--muted)">${doneCnt}/5 этапов</span></div>
|
<div style="display:flex;align-items:center;margin-bottom:6px"><b style="font-size:13px;display:inline-flex;align-items:center;gap:7px">${ic('activity',15)} Прогресс проекта</b><span style="margin-left:auto;font-size:11px;color:var(--muted)">${doneCnt}/5 этапов</span></div>
|
||||||
${CLIENT_STAGES.map((s,i)=>{
|
${CLIENT_STAGES.map((s,i)=>{
|
||||||
const done=doneArr[i], cur=!done&&i===firstUndone;
|
const done=doneArr[i], cur=!done&&i===firstUndone;
|
||||||
const brd=done?'#047857':cur?'#10B981':'#E5E7EB';
|
const brd=done?'#047857':cur?'#10B981':'#E5E7EB';
|
||||||
@ -691,7 +703,7 @@ function renderMainPanel(){
|
|||||||
<div class="cc-fl">Контакт</div>
|
<div class="cc-fl">Контакт</div>
|
||||||
<input class="cc-fi" id="cpContact" value="${esc(crm.contact||'')}" placeholder="телефон / email" onchange="saveCrm()" style="margin-bottom:12px">
|
<input class="cc-fi" id="cpContact" value="${esc(crm.contact||'')}" placeholder="телефон / email" onchange="saveCrm()" style="margin-bottom:12px">
|
||||||
<div class="cc-fl" style="display:flex;align-items:center;justify-content:space-between">Заметка ${micBtn('cpNote',26)}</div>
|
<div class="cc-fl" style="display:flex;align-items:center;justify-content:space-between">Заметка ${micBtn('cpNote',26)}</div>
|
||||||
<textarea id="cpNote" onchange="saveCrm()" placeholder="заметки по клиенту… или 🎤" style="width:100%;border:none;outline:none;background:transparent;font-family:Inter;font-size:13px;resize:vertical;min-height:46px;color:var(--text)">${esc(crm.note||'')}</textarea>
|
<textarea id="cpNote" onchange="saveCrm()" placeholder="заметки по клиенту… или голосом" style="width:100%;border:none;outline:none;background:transparent;font-family:Inter;font-size:13px;resize:vertical;min-height:46px;color:var(--text)">${esc(crm.note||'')}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -706,15 +718,15 @@ function renderMainPanel(){
|
|||||||
else if(mainTab==="deviations"){p.innerHTML=renderDeviations();}
|
else if(mainTab==="deviations"){p.innerHTML=renderDeviations();}
|
||||||
else if(mainTab==="suggestions"){p.innerHTML=renderSuggestions();}
|
else if(mainTab==="suggestions"){p.innerHTML=renderSuggestions();}
|
||||||
}
|
}
|
||||||
const CRM_SUG_ST={new:['🆕','#EFF6FF','#2563EB'],discussion:['💬','#FEF3C7','#92400E'],accepted:['✅','#D1FAE5','#047857'],rejected:['❌','#FEE2E2','#B91C1C']};
|
const CRM_SUG_ST={new:[ic('dot',13),'#EFF6FF','#2563EB'],discussion:[ic('message',13),'#FEF3C7','#92400E'],accepted:[ic('checkCircle',13),'#D1FAE5','#047857'],rejected:[ic('close',13),'#FEE2E2','#B91C1C']};
|
||||||
const SUG_STATUSES=[['new','🆕 Новое'],['discussion','💬 Обсуждаем'],['accepted','✅ Принять'],['rejected','❌ Отклонить']];
|
const SUG_STATUSES=[['new',ic('dot',13)+' Новое'],['discussion',ic('message',13)+' Обсуждаем'],['accepted',ic('checkCircle',13)+' Принять'],['rejected',ic('close',13)+' Отклонить']];
|
||||||
function renderSuggestions(){
|
function renderSuggestions(){
|
||||||
const sgs=state.suggestions||[];
|
const sgs=state.suggestions||[];
|
||||||
if(!sgs.length)return `<div class="run-card" style="background:#fff;border:1px solid #E5E7EB;border-radius:12px;padding:22px;text-align:center"><div style="font-size:26px;margin-bottom:6px">💡</div><div style="font-size:14px;font-weight:700">Предложений нет</div><div style="font-size:12px;color:#6B7280;margin-top:4px">Идеи клиента появятся здесь — из кабинета («Мои идеи») или когда Елена поймает предложение в диалоге. Меняешь статус и пишешь решение — клиент видит его в кабинете.</div></div>`;
|
if(!sgs.length)return `<div class="run-card" style="background:#fff;border:1px solid #E5E7EB;border-radius:12px;padding:22px;text-align:center"><div style="color:#9CA3AF;margin-bottom:6px;display:flex;justify-content:center">${ic('idea',28)}</div><div style="font-size:14px;font-weight:700">Предложений нет</div><div style="font-size:12px;color:#6B7280;margin-top:4px">Идеи клиента появятся здесь — из кабинета («Мои идеи») или когда Елена поймает предложение в диалоге. Меняешь статус и пишешь решение — клиент видит его в кабинете.</div></div>`;
|
||||||
return `<div style="font-size:12px;color:#6B7280;margin-bottom:12px">Идеи клиента — не игнорируем, а рассматриваем. Статус и решение видны клиенту: чувствует, что услышан.</div>`+sgs.map(s=>{
|
return `<div style="font-size:12px;color:#6B7280;margin-bottom:12px">Идеи клиента — не игнорируем, а рассматриваем. Статус и решение видны клиенту: чувствует, что услышан.</div>`+sgs.map(s=>{
|
||||||
const st=CRM_SUG_ST[s.status]||CRM_SUG_ST.new;
|
const st=CRM_SUG_ST[s.status]||CRM_SUG_ST.new;
|
||||||
return `<div class="blk" style="border-left:3px solid ${st[2]};padding:13px 15px;margin-bottom:10px">
|
return `<div class="blk" style="border-left:3px solid ${st[2]};padding:13px 15px;margin-bottom:10px">
|
||||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap"><span style="font-size:10px;font-weight:700;color:#6B7280;background:#F3F4F6;padding:2px 8px;border-radius:5px">${s.source==='client'?'👤 клиент':'✍ оператор'}${s.topic?' · '+esc(s.topic):''}</span><span style="margin-left:auto;font-size:10px;color:#9CA3AF">${(s.at||'').slice(0,10).split('-').reverse().join('.')}</span></div>
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap"><span style="font-size:10px;font-weight:700;color:#6B7280;background:#F3F4F6;padding:2px 8px;border-radius:5px">${s.source==='client'?ic('user',12)+' клиент':ic('edit',12)+' оператор'}${s.topic?' · '+esc(s.topic):''}</span><span style="margin-left:auto;font-size:10px;color:#9CA3AF">${(s.at||'').slice(0,10).split('-').reverse().join('.')}</span></div>
|
||||||
<div style="font-size:14px;font-weight:600;color:#1A1A2E;margin-bottom:10px">${esc(s.text)}</div>
|
<div style="font-size:14px;font-weight:600;color:#1A1A2E;margin-bottom:10px">${esc(s.text)}</div>
|
||||||
<div style="display:flex;gap:5px;flex-wrap:wrap;margin-bottom:8px">${SUG_STATUSES.map(([k,n])=>`<button onclick="setSugStatus('${s.id}','${k}')" style="font-size:11px;font-weight:700;padding:5px 10px;border-radius:7px;cursor:pointer;border:1.5px solid ${s.status===k?CRM_SUG_ST[k][2]:'#E5E7EB'};background:${s.status===k?CRM_SUG_ST[k][1]:'#fff'};color:${s.status===k?CRM_SUG_ST[k][2]:'#6B7280'}">${n}</button>`).join('')}</div>
|
<div style="display:flex;gap:5px;flex-wrap:wrap;margin-bottom:8px">${SUG_STATUSES.map(([k,n])=>`<button onclick="setSugStatus('${s.id}','${k}')" style="font-size:11px;font-weight:700;padding:5px 10px;border-radius:7px;cursor:pointer;border:1.5px solid ${s.status===k?CRM_SUG_ST[k][2]:'#E5E7EB'};background:${s.status===k?CRM_SUG_ST[k][1]:'#fff'};color:${s.status===k?CRM_SUG_ST[k][2]:'#6B7280'}">${n}</button>`).join('')}</div>
|
||||||
<textarea placeholder="Решение / обоснование для клиента…" onchange="setSugDecision('${s.id}',this.value)" style="width:100%;border:1.5px solid var(--border);border-radius:8px;padding:8px 10px;font-size:12px;font-family:Inter;resize:vertical;min-height:38px;outline:none">${esc(s.decision||'')}</textarea>
|
<textarea placeholder="Решение / обоснование для клиента…" onchange="setSugDecision('${s.id}',this.value)" style="width:100%;border:1.5px solid var(--border);border-radius:8px;padding:8px 10px;font-size:12px;font-family:Inter;resize:vertical;min-height:38px;outline:none">${esc(s.decision||'')}</textarea>
|
||||||
@ -812,7 +824,7 @@ function syncPaymentReminders(){
|
|||||||
const need=due[s.key]&&(sp[s.key]||0)>0&&!pays[s.key];
|
const need=due[s.key]&&(sp[s.key]||0)>0&&!pays[s.key];
|
||||||
const idx=state.tasks.findIndex(t=>t.auto===marker);
|
const idx=state.tasks.findIndex(t=>t.auto===marker);
|
||||||
if(need){
|
if(need){
|
||||||
const text=`💳 Получить оплату за «${s.name}» — ${money(sp[s.key])}`;
|
const text=`Получить оплату за «${s.name}» — ${money(sp[s.key])}`;
|
||||||
if(idx<0){state.tasks.push({text,due:due[s.key],done:false,auto:marker});changed=true;}
|
if(idx<0){state.tasks.push({text,due:due[s.key],done:false,auto:marker});changed=true;}
|
||||||
else if(state.tasks[idx].due!==due[s.key]||state.tasks[idx].text!==text||state.tasks[idx].done){
|
else if(state.tasks[idx].due!==due[s.key]||state.tasks[idx].text!==text||state.tasks[idx].done){
|
||||||
state.tasks[idx].due=due[s.key];state.tasks[idx].text=text;state.tasks[idx].done=false;changed=true;}
|
state.tasks[idx].due=due[s.key];state.tasks[idx].text=text;state.tasks[idx].done=false;changed=true;}
|
||||||
@ -827,8 +839,8 @@ function alignDealToEstimate(){
|
|||||||
saveEstimate();renderClient();
|
saveEstimate();renderClient();
|
||||||
}
|
}
|
||||||
// ── Способы оплаты ──
|
// ── Способы оплаты ──
|
||||||
const PAY_METHODS=[["bank","🏦 Безнал"],["cash","💵 Наличные"],["sbp","📱 СБП"],["card","💳 Карта"]];
|
const PAY_METHODS=[["bank","Безнал"],["cash","Наличные"],["sbp","СБП"],["card","Карта"]];
|
||||||
const METHOD_ICON={bank:"🏦",cash:"💵",sbp:"📱",card:"💳"};
|
const METHOD_ICON={bank:ic('card',13),cash:ic('cash',13),sbp:ic('phone',13),card:ic('card',13)};
|
||||||
const METHOD_NAME=Object.fromEntries(PAY_METHODS.map(m=>[m[0],m[1]]));
|
const METHOD_NAME=Object.fromEntries(PAY_METHODS.map(m=>[m[0],m[1]]));
|
||||||
function methodSelect(id,cur){return `<select id="${id}" style="border:1.5px solid var(--border);border-radius:8px;padding:7px 9px;font-size:13px;font-family:Inter;background:var(--white)">${PAY_METHODS.map(([k,n])=>`<option value="${k}" ${cur===k?'selected':''}>${n}</option>`).join('')}</select>`;}
|
function methodSelect(id,cur){return `<select id="${id}" style="border:1.5px solid var(--border);border-radius:8px;padding:7px 9px;font-size:13px;font-family:Inter;background:var(--white)">${PAY_METHODS.map(([k,n])=>`<option value="${k}" ${cur===k?'selected':''}>${n}</option>`).join('')}</select>`;}
|
||||||
async function saveEstimate(){
|
async function saveEstimate(){
|
||||||
@ -898,7 +910,7 @@ function showQRModal(d,name,amount){
|
|||||||
ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:9999';
|
ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:9999';
|
||||||
const qrBox=d.qr_image?`<img src="${d.qr_image}" style="width:220px;height:220px;border-radius:12px">`:`<div style="width:220px;height:220px;margin:0 auto;border:2px dashed #A7F3D0;border-radius:12px;display:flex;align-items:center;justify-content:center;text-align:center;color:#6B7280;font-size:12px;padding:16px">QR придёт от Сбера<br>после настройки реквизитов<br><br>(демо-режим)</div>`;
|
const qrBox=d.qr_image?`<img src="${d.qr_image}" style="width:220px;height:220px;border-radius:12px">`:`<div style="width:220px;height:220px;margin:0 auto;border:2px dashed #A7F3D0;border-radius:12px;display:flex;align-items:center;justify-content:center;text-align:center;color:#6B7280;font-size:12px;padding:16px">QR придёт от Сбера<br>после настройки реквизитов<br><br>(демо-режим)</div>`;
|
||||||
ov.innerHTML=`<div style="background:#fff;border-radius:16px;padding:24px;max-width:340px;width:90%;text-align:center;font-family:Inter">
|
ov.innerHTML=`<div style="background:#fff;border-radius:16px;padding:24px;max-width:340px;width:90%;text-align:center;font-family:Inter">
|
||||||
<div style="font-size:15px;font-weight:800;font-family:Montserrat;margin-bottom:4px">📱 СБП · ${esc(name)}</div>
|
<div style="font-size:15px;font-weight:800;font-family:Montserrat;margin-bottom:4px;display:flex;align-items:center;gap:7px">${ic('phone',16)} СБП · ${esc(name)}</div>
|
||||||
<div style="font-size:22px;font-weight:800;color:var(--primary);margin-bottom:14px">${money(amount)}</div>
|
<div style="font-size:22px;font-weight:800;color:var(--primary);margin-bottom:14px">${money(amount)}</div>
|
||||||
${qrBox}
|
${qrBox}
|
||||||
${d.demo?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:8px;padding:8px;margin-top:12px">${esc(d.message||'Демо-режим')}</div>`:''}
|
${d.demo?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:8px;padding:8px;margin-top:12px">${esc(d.message||'Демо-режим')}</div>`:''}
|
||||||
@ -965,7 +977,7 @@ function renderPaymentPlan(){
|
|||||||
<span style="font-size:11px;font-weight:700;color:var(--primary);background:#ECFDF5;padding:3px 9px;border-radius:6px">${CX_SHORT[cx]}</span>
|
<span style="font-size:11px;font-weight:700;color:var(--primary);background:#ECFDF5;padding:3px 9px;border-radius:6px">${CX_SHORT[cx]}</span>
|
||||||
<button class="cp-btn cp-r" style="padding:4px 9px;margin-left:auto" onclick="buildEstimate()">↻ Пересчитать</button>
|
<button class="cp-btn cp-r" style="padding:4px 9px;margin-left:auto" onclick="buildEstimate()">↻ Пересчитать</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:11px;color:var(--muted);margin-bottom:12px">Получено <b style="color:var(--primary)">${money(paidTotal)}</b> из ${money(target)} · <b>✎</b> цена · <b style="color:#DC2626">↺</b> отмена оплаты · 📅 срок оплаты этапа</div>
|
<div style="font-size:11px;color:var(--muted);margin-bottom:12px">Получено <b style="color:var(--primary)">${money(paidTotal)}</b> из ${money(target)} · <b>✎</b> цена · <b style="color:#DC2626">↺</b> отмена оплаты · ${ic('calendar',12)} срок оплаты этапа</div>
|
||||||
${CLIENT_STAGES.map(s=>{
|
${CLIENT_STAGES.map(s=>{
|
||||||
const done=stageIsDone(s.key);
|
const done=stageIsDone(s.key);
|
||||||
const paid=pays[s.key];
|
const paid=pays[s.key];
|
||||||
@ -983,14 +995,14 @@ function renderPaymentPlan(){
|
|||||||
} else if(done){
|
} else if(done){
|
||||||
badge=`<span style="font-size:10px;font-weight:700;color:#92400E;background:#FEF3C7;padding:1px 6px;border-radius:4px;margin-left:4px">Доступен к оплате</span>`;
|
badge=`<span style="font-size:10px;font-weight:700;color:#92400E;background:#FEF3C7;padding:1px 6px;border-radius:4px;margin-left:4px">Доступен к оплате</span>`;
|
||||||
sub=`<div style="font-size:11px;color:var(--muted);margin-top:2px">${esc(s.desc)}</div>`;
|
sub=`<div style="font-size:11px;color:var(--muted);margin-top:2px">${esc(s.desc)}</div>`;
|
||||||
action=`<button class="cp-btn cp-r" style="white-space:nowrap;padding:7px 11px" onclick="sberQR('${s.key}')" title="Выставить СБП QR">📱 QR</button><button class="cp-btn cp-a" style="white-space:nowrap" onclick="markStagePayInput('${s.key}')">Оплата · ${money(price)}</button>`;
|
action=`<button class="cp-btn cp-r" style="white-space:nowrap;padding:7px 11px" onclick="sberQR('${s.key}')" title="Выставить СБП QR">${ic('phone',13)} QR</button><button class="cp-btn cp-a" style="white-space:nowrap" onclick="markStagePayInput('${s.key}')">Оплата · ${money(price)}</button>`;
|
||||||
} else {
|
} else {
|
||||||
badge=`<span style="font-size:10px;font-weight:700;color:#9CA3AF;background:#F3F4F6;padding:1px 6px;border-radius:4px;margin-left:4px">🔒 Ожидает</span>`;
|
badge=`<span style="font-size:10px;font-weight:700;color:#9CA3AF;background:#F3F4F6;padding:1px 6px;border-radius:4px;margin-left:4px">${ic('lock',11)} Ожидает</span>`;
|
||||||
sub=`<div style="font-size:11px;color:#CBD5E1;margin-top:2px">${esc(s.desc)}</div>`;
|
sub=`<div style="font-size:11px;color:#CBD5E1;margin-top:2px">${esc(s.desc)}</div>`;
|
||||||
action=`<span onclick="editStagePrice('${s.key}')" style="cursor:pointer;font-size:11px;font-weight:700;color:#9CA3AF;white-space:nowrap;border-bottom:1px dashed #CBD5E1">${money(price)}</span>`;
|
action=`<span onclick="editStagePrice('${s.key}')" style="cursor:pointer;font-size:11px;font-weight:700;color:#9CA3AF;white-space:nowrap;border-bottom:1px dashed #CBD5E1">${money(price)}</span>`;
|
||||||
}
|
}
|
||||||
const dd=due[s.key]||'';
|
const dd=due[s.key]||'';
|
||||||
const dueChip=dd?`<span style="font-size:10px;font-weight:600;color:#2563EB;background:#EFF6FF;padding:1px 6px;border-radius:4px;margin-left:6px">📅 до ${fmtDate(dd)}</span>`:'';
|
const dueChip=dd?`<span style="font-size:10px;font-weight:600;color:#2563EB;background:#EFF6FF;padding:1px 6px;border-radius:4px;margin-left:6px">${ic('calendar',11)} до ${fmtDate(dd)}</span>`:'';
|
||||||
const dueInput=isFree?'':`<input type="date" value="${dd}" title="Срок оплаты этапа" onchange="setStageDue('${s.key}',this.value)" style="border:1.5px solid var(--border);border-radius:7px;padding:5px 7px;font-size:11px;font-family:Inter;color:#6B7280;width:130px">`;
|
const dueInput=isFree?'':`<input type="date" value="${dd}" title="Срок оплаты этапа" onchange="setStageDue('${s.key}',this.value)" style="border:1.5px solid var(--border);border-radius:7px;padding:5px 7px;font-size:11px;font-family:Inter;color:#6B7280;width:130px">`;
|
||||||
return `<div id="stagerow-${s.key}" style="display:flex;align-items:center;gap:12px;padding:11px 0;border-top:1px solid var(--bg)">
|
return `<div id="stagerow-${s.key}" style="display:flex;align-items:center;gap:12px;padding:11px 0;border-top:1px solid var(--bg)">
|
||||||
<span style="width:32px;height:32px;border-radius:8px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:15px;background:${paid?'#D1FAE5':done||isFree?'#ECFDF5':'#F9FAFB'};border:1.5px solid ${paid?'#6EE7B7':done||isFree?'#A7F3D0':'#E5E7EB'}">${s.icon}</span>
|
<span style="width:32px;height:32px;border-radius:8px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:15px;background:${paid?'#D1FAE5':done||isFree?'#ECFDF5':'#F9FAFB'};border:1.5px solid ${paid?'#6EE7B7':done||isFree?'#A7F3D0':'#E5E7EB'}">${s.icon}</span>
|
||||||
@ -1011,20 +1023,20 @@ function renderPaymentPlan(){
|
|||||||
<span style="margin-left:auto;font-weight:700;color:var(--primary)">${money(target)}</span>
|
<span style="margin-left:auto;font-weight:700;color:var(--primary)">${money(target)}</span>
|
||||||
</div>
|
</div>
|
||||||
${unalloc!==0?`<div style="display:flex;align-items:center;gap:10px;padding:8px 12px;margin-top:4px;border-radius:9px;background:${unalloc>0?'#FFF7ED':'#FEF2F2'};border:1px solid ${unalloc>0?'#FDE68A':'#FECACA'}">
|
${unalloc!==0?`<div style="display:flex;align-items:center;gap:10px;padding:8px 12px;margin-top:4px;border-radius:9px;background:${unalloc>0?'#FFF7ED':'#FEF2F2'};border:1px solid ${unalloc>0?'#FDE68A':'#FECACA'}">
|
||||||
<span style="font-size:12px;font-weight:700;color:${unalloc>0?'#92400E':'#DC2626'}">${unalloc>0?'⚠ Нераспределено':'⚠ Перебор сметы'}: ${money(Math.abs(unalloc))}</span>
|
<span style="font-size:12px;font-weight:700;color:${unalloc>0?'#92400E':'#DC2626'}">${unalloc>0?ic('alert',12)+' Нераспределено':ic('alert',12)+' Перебор сметы'}: ${money(Math.abs(unalloc))}</span>
|
||||||
<button class="cp-btn cp-r" style="margin-left:auto;padding:4px 10px" onclick="alignDealToEstimate()">Подогнать сделку под смету</button>
|
<button class="cp-btn cp-r" style="margin-left:auto;padding:4px 10px" onclick="alignDealToEstimate()">Подогнать сделку под смету</button>
|
||||||
</div>`:''}
|
</div>`:''}
|
||||||
<div style="margin-top:12px;padding:11px 14px;border-radius:10px;font-size:12px;line-height:1.5;${isUnlocked?'background:#ECFDF5;border:1px solid #6EE7B7;color:#047857':'background:#FFF7ED;border:1px solid #FDE68A;color:#92400E'}">
|
<div style="margin-top:12px;padding:11px 14px;border-radius:10px;font-size:12px;line-height:1.5;${isUnlocked?'background:#ECFDF5;border:1px solid #6EE7B7;color:#047857':'background:#FFF7ED;border:1px solid #FDE68A;color:#92400E'}">
|
||||||
${isUnlocked
|
${isUnlocked
|
||||||
?`✅ <b>Оплата получена.</b> Клиент может скачать ТЗ и печатные документы.`
|
?`${ic('checkCircle',14)} <b>Оплата получена.</b> Клиент может скачать ТЗ и печатные документы.`
|
||||||
:`🔒 <b>Правило:</b> ТЗ (печать + выгрузка) выдаётся клиенту только после полной оплаты — ${money(target)}.`}
|
:`${ic('lock',13)} <b>Правило:</b> ТЗ (печать + выгрузка) выдаётся клиенту только после полной оплаты — ${money(target)}.`}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
function renderPricing(){
|
function renderPricing(){
|
||||||
const box=document.getElementById("pricingBox");if(!box)return;
|
const box=document.getElementById("pricingBox");if(!box)return;
|
||||||
const billing=(state.crm||{}).billing_type||"paid";
|
const billing=(state.crm||{}).billing_type||"paid";
|
||||||
const freeNote=billing==="free"?`<div class="blk" style="background:#EEF2FF;border-color:#C7D2FE;padding:12px 14px;margin-bottom:14px;font-size:13px;line-height:1.5"><b style="color:#6366F1">🎁 Бесплатный клиент.</b> Расчёт цены ниже — справочно: показывает рыночную стоимость и аргументацию ценности. Решение об оплате остаётся на ваше усмотрение.</div>`:'';
|
const freeNote=billing==="free"?`<div class="blk" style="background:#EEF2FF;border-color:#C7D2FE;padding:12px 14px;margin-bottom:14px;font-size:13px;line-height:1.5"><b style="color:#6366F1">${ic('gift',14)} Бесплатный клиент.</b> Расчёт цены ниже — справочно: показывает рыночную стоимость и аргументацию ценности. Решение об оплате остаётся на ваше усмотрение.</div>`:'';
|
||||||
const p=state.pricing;
|
const p=state.pricing;
|
||||||
if(!p){box.innerHTML=freeNote+`<div class="run-card" style="margin:0 0 18px"><div class="run-ic">${ic('wallet',30)}</div><div class="run-t">Ценовое предложение</div><div class="run-d">Елена оценит масштаб работы, проанализирует рынок и предложит пакеты с аргументами цены.</div><button class="run-btn" id="rb-pricing" onclick="buildPricing()">Рассчитать цену →</button></div>`;return}
|
if(!p){box.innerHTML=freeNote+`<div class="run-card" style="margin:0 0 18px"><div class="run-ic">${ic('wallet',30)}</div><div class="run-t">Ценовое предложение</div><div class="run-d">Елена оценит масштаб работы, проанализирует рынок и предложит пакеты с аргументами цены.</div><button class="run-btn" id="rb-pricing" onclick="buildPricing()">Рассчитать цену →</button></div>`;return}
|
||||||
box.innerHTML=freeNote+pricingCardHTML(p);
|
box.innerHTML=freeNote+pricingCardHTML(p);
|
||||||
@ -1064,7 +1076,7 @@ function renderPayments(){
|
|||||||
${deal>0?`<span style="margin-left:auto;font-size:12px;color:var(--muted)">Сделка ${money(deal)} · Получено ${money(paid)} · <b style="color:${left>0?'#DC2626':'#047857'}">Остаток ${money(left)}</b></span>`:'<span style="margin-left:auto"></span>'}
|
${deal>0?`<span style="margin-left:auto;font-size:12px;color:var(--muted)">Сделка ${money(deal)} · Получено ${money(paid)} · <b style="color:${left>0?'#DC2626':'#047857'}">Остаток ${money(left)}</b></span>`:'<span style="margin-left:auto"></span>'}
|
||||||
${left>0?`<button class="cp-btn cp-r" style="padding:5px 10px" onclick="remindBalance(${left})">⏰ Задача на остаток</button>`:''}
|
${left>0?`<button class="cp-btn cp-r" style="padding:5px 10px" onclick="remindBalance(${left})">⏰ Задача на остаток</button>`:''}
|
||||||
${pays.length?`<button class="cp-btn cp-r" style="padding:5px 10px" onclick="exportPayments()">⬇ CSV</button>`:''}</div>
|
${pays.length?`<button class="cp-btn cp-r" style="padding:5px 10px" onclick="exportPayments()">⬇ CSV</button>`:''}</div>
|
||||||
${paid>0?`<div style="display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap"><span style="font-size:11px;font-weight:700;color:#047857;background:#ECFDF5;padding:3px 10px;border-radius:6px">💵 Наличные ${money(cash)}</span><span style="font-size:11px;font-weight:700;color:#2563EB;background:#EFF6FF;padding:3px 10px;border-radius:6px">🏦 Безнал ${money(noncash)}</span></div>`:''}
|
${paid>0?`<div style="display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap"><span style="font-size:11px;font-weight:700;color:#047857;background:#ECFDF5;padding:3px 10px;border-radius:6px">${ic('cash',12)} Наличные ${money(cash)}</span><span style="font-size:11px;font-weight:700;color:#2563EB;background:#EFF6FF;padding:3px 10px;border-radius:6px">${ic('card',12)} Безнал ${money(noncash)}</span></div>`:''}
|
||||||
${pays.map((p,i)=>i===editPayIdx
|
${pays.map((p,i)=>i===editPayIdx
|
||||||
?`<div style="display:flex;align-items:center;gap:8px;padding:9px 10px;border-top:1px solid var(--bg);flex-wrap:wrap;background:var(--bg);border-radius:8px;margin-top:4px">
|
?`<div style="display:flex;align-items:center;gap:8px;padding:9px 10px;border-top:1px solid var(--bg);flex-wrap:wrap;background:var(--bg);border-radius:8px;margin-top:4px">
|
||||||
<input type="date" id="epd-${i}" value="${esc(p.date||'')}" style="border:1.5px solid var(--border);border-radius:8px;padding:6px 8px;font-size:13px;font-family:Inter">
|
<input type="date" id="epd-${i}" value="${esc(p.date||'')}" style="border:1.5px solid var(--border);border-radius:8px;padding:6px 8px;font-size:13px;font-family:Inter">
|
||||||
@ -1075,7 +1087,7 @@ function renderPayments(){
|
|||||||
<button class="cp-btn cp-a" style="padding:6px 12px" onclick="confirmEditPayment(${i})">Сохранить</button>
|
<button class="cp-btn cp-a" style="padding:6px 12px" onclick="confirmEditPayment(${i})">Сохранить</button>
|
||||||
<button class="cp-btn cp-r" style="padding:6px 10px" onclick="cancelEditPayment()">✕</button>
|
<button class="cp-btn cp-r" style="padding:6px 10px" onclick="cancelEditPayment()">✕</button>
|
||||||
</div>`
|
</div>`
|
||||||
:`<div style="display:flex;align-items:center;gap:10px;padding:7px 0;border-top:1px solid var(--bg)"><span style="font-size:12px;color:var(--muted);min-width:70px">${esc(p.date||'')}</span><span title="${esc(METHOD_NAME[p.method||'bank']||'')}" style="font-size:13px">${METHOD_ICON[p.method||'bank']||'🏦'}</span><span style="flex:1;font-size:13px">${esc(p.note||'Платёж')}${p.stage?` <span style="font-size:10px;color:#9CA3AF">· этап</span>`:''}</span><span style="font-size:13px;font-weight:700;color:#047857">${money(p.amount)}</span><span onclick="editPayment(${i})" title="Изменить" style="cursor:pointer;color:#9CA3AF;font-size:13px">✎</span><button onclick="delPayment(${i})" title="Удалить" style="border:none;background:none;cursor:pointer;color:#cbd5e1;font-size:14px">✕</button></div>`).join("")||'<div style="font-size:12px;color:#cbd5e1;padding:4px">Платежей нет</div>'}
|
:`<div style="display:flex;align-items:center;gap:10px;padding:7px 0;border-top:1px solid var(--bg)"><span style="font-size:12px;color:var(--muted);min-width:70px">${esc(p.date||'')}</span><span title="${esc(METHOD_NAME[p.method||'bank']||'')}" style="font-size:13px;color:#047857">${METHOD_ICON[p.method||'bank']||ic('card',13)}</span><span style="flex:1;font-size:13px">${esc(p.note||'Платёж')}${p.stage?` <span style="font-size:10px;color:#9CA3AF">· этап</span>`:''}</span><span style="font-size:13px;font-weight:700;color:#047857">${money(p.amount)}</span><span onclick="editPayment(${i})" title="Изменить" style="cursor:pointer;color:#9CA3AF;font-size:13px">✎</span><button onclick="delPayment(${i})" title="Удалить" style="border:none;background:none;cursor:pointer;color:#cbd5e1;font-size:14px">✕</button></div>`).join("")||'<div style="font-size:12px;color:#cbd5e1;padding:4px">Платежей нет</div>'}
|
||||||
<div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;align-items:center">
|
<div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;align-items:center">
|
||||||
<input id="payDate" type="date" value="${new Date().toISOString().slice(0,10)}" style="border:1.5px solid var(--border);border-radius:8px;padding:8px;font-size:13px;font-family:Inter">
|
<input id="payDate" type="date" value="${new Date().toISOString().slice(0,10)}" style="border:1.5px solid var(--border);border-radius:8px;padding:8px;font-size:13px;font-family:Inter">
|
||||||
<input id="payAmt" type="number" placeholder="Сумма ₽" style="width:110px;border:1.5px solid var(--border);border-radius:8px;padding:8px 11px;font-size:13px;font-family:Inter" onkeydown="if(event.key==='Enter')addPayment()">
|
<input id="payAmt" type="number" placeholder="Сумма ₽" style="width:110px;border:1.5px solid var(--border);border-radius:8px;padding:8px 11px;font-size:13px;font-family:Inter" onkeydown="if(event.key==='Enter')addPayment()">
|
||||||
@ -1154,7 +1166,7 @@ function renderTasks(){
|
|||||||
box.innerHTML=`<div style="background:var(--white);border:1.5px solid var(--border);border-radius:12px;padding:14px 16px;margin-bottom:18px">
|
box.innerHTML=`<div style="background:var(--white);border:1.5px solid var(--border);border-radius:12px;padding:14px 16px;margin-bottom:18px">
|
||||||
<div style="font-size:13px;font-weight:700;margin-bottom:10px;display:flex;align-items:center;gap:8px">${ic('pin',16)} Задачи по клиенту</div>
|
<div style="font-size:13px;font-weight:700;margin-bottom:10px;display:flex;align-items:center;gap:8px">${ic('pin',16)} Задачи по клиенту</div>
|
||||||
${tasks.map((t,i)=>`<div style="display:flex;align-items:center;gap:10px;padding:7px 0;border-top:1px solid var(--bg)"><input type="checkbox" ${t.done?'checked':''} onchange="toggleTask(${i})" style="width:16px;height:16px;cursor:pointer"><span style="flex:1;font-size:13px;${t.done?'text-decoration:line-through;color:#9ca3af':''}">${esc(t.text)}</span>${t.due?`<span style="font-size:11px;font-weight:600;padding:2px 8px;border-radius:6px;${!t.done&&t.due<today?'background:#FEF2F2;color:#DC2626':t.due===today?'background:#FEF3C7;color:#92400E':'background:#F1F5F9;color:#6B7280'}">${t.due===today?'сегодня':fmtDate(t.due)}</span>`:''}<button onclick="delTask(${i})" style="border:none;background:none;cursor:pointer;color:#cbd5e1;font-size:14px">✕</button></div>`).join("")||'<div style="font-size:12px;color:#cbd5e1;padding:4px">Задач нет</div>'}
|
${tasks.map((t,i)=>`<div style="display:flex;align-items:center;gap:10px;padding:7px 0;border-top:1px solid var(--bg)"><input type="checkbox" ${t.done?'checked':''} onchange="toggleTask(${i})" style="width:16px;height:16px;cursor:pointer"><span style="flex:1;font-size:13px;${t.done?'text-decoration:line-through;color:#9ca3af':''}">${esc(t.text)}</span>${t.due?`<span style="font-size:11px;font-weight:600;padding:2px 8px;border-radius:6px;${!t.done&&t.due<today?'background:#FEF2F2;color:#DC2626':t.due===today?'background:#FEF3C7;color:#92400E':'background:#F1F5F9;color:#6B7280'}">${t.due===today?'сегодня':fmtDate(t.due)}</span>`:''}<button onclick="delTask(${i})" style="border:none;background:none;cursor:pointer;color:#cbd5e1;font-size:14px">✕</button></div>`).join("")||'<div style="font-size:12px;color:#cbd5e1;padding:4px">Задач нет</div>'}
|
||||||
<div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap"><input id="newTask" placeholder="Новая задача… или 🎤" style="flex:1;min-width:140px;border:1.5px solid var(--border);border-radius:8px;padding:8px 11px;font-size:13px;font-family:Inter;outline:none" onkeydown="if(event.key==='Enter')addTask()">${micBtn('newTask',34)}<input id="newTaskDue" type="date" style="border:1.5px solid var(--border);border-radius:8px;padding:8px;font-size:13px;font-family:Inter"><button class="cp-btn cp-a" onclick="addTask()">+ Добавить</button></div>
|
<div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap"><input id="newTask" placeholder="Новая задача… или голосом" style="flex:1;min-width:140px;border:1.5px solid var(--border);border-radius:8px;padding:8px 11px;font-size:13px;font-family:Inter;outline:none" onkeydown="if(event.key==='Enter')addTask()">${micBtn('newTask',34)}<input id="newTaskDue" type="date" style="border:1.5px solid var(--border);border-radius:8px;padding:8px;font-size:13px;font-family:Inter"><button class="cp-btn cp-a" onclick="addTask()">+ Добавить</button></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
function fmtDate(d){const[y,m,dd]=d.split("-");return `${dd}.${m}`}
|
function fmtDate(d){const[y,m,dd]=d.split("-");return `${dd}.${m}`}
|
||||||
@ -1190,9 +1202,9 @@ function exportSpecPDF(){
|
|||||||
body+=H("Модули системы");s.modules.forEach(m=>{body+=`<div class="item"><div><span class="tag">${esc(m.source_node)}</span> <b>${esc(m.name)}</b></div><div class="muted">${esc(m.purpose)}</div><div style="margin:6px 0"><b>Экраны:</b> ${m.screens.map(esc).join(", ")}</div><div><b>Вход:</b> ${esc(m.inputs_data)}</div><div><b>Выход:</b> ${esc(m.outputs_data)}</div>${m.rules.length?`<div style="margin-top:5px"><b>Бизнес-правила:</b><ul>${m.rules.map(r=>`<li>${esc(r)}</li>`).join("")}</ul></div>`:''}</div>`});
|
body+=H("Модули системы");s.modules.forEach(m=>{body+=`<div class="item"><div><span class="tag">${esc(m.source_node)}</span> <b>${esc(m.name)}</b></div><div class="muted">${esc(m.purpose)}</div><div style="margin:6px 0"><b>Экраны:</b> ${m.screens.map(esc).join(", ")}</div><div><b>Вход:</b> ${esc(m.inputs_data)}</div><div><b>Выход:</b> ${esc(m.outputs_data)}</div>${m.rules.length?`<div style="margin-top:5px"><b>Бизнес-правила:</b><ul>${m.rules.map(r=>`<li>${esc(r)}</li>`).join("")}</ul></div>`:''}</div>`});
|
||||||
body+=H("Модель данных");s.entities.forEach(e=>{body+=`<div class="item"><b>◆ ${esc(e.name)}</b><table><tr><th>Поле</th><th>Тип</th></tr>${e.fields.map(f=>`<tr><td>${esc(f.field)}</td><td>${esc(f.type)}</td></tr>`).join("")}</table>${e.relations.length?`<div class="muted">Связи: ${e.relations.map(esc).join("; ")}</div>`:''}<div class="ex">${esc(e.example)}</div></div>`});
|
body+=H("Модель данных");s.entities.forEach(e=>{body+=`<div class="item"><b>◆ ${esc(e.name)}</b><table><tr><th>Поле</th><th>Тип</th></tr>${e.fields.map(f=>`<tr><td>${esc(f.field)}</td><td>${esc(f.type)}</td></tr>`).join("")}</table>${e.relations.length?`<div class="muted">Связи: ${e.relations.map(esc).join("; ")}</div>`:''}<div class="ex">${esc(e.example)}</div></div>`});
|
||||||
// Оргструктура
|
// Оргструктура
|
||||||
if(state.orgchart&&(state.orgchart.units||[]).length){body+=H("Оргструктура");body+=`<div class="item muted">${esc(state.orgchart.insight||'')}</div>`;state.orgchart.units.forEach(u=>{body+=`<div class="item"><b>${esc(u.role)}</b> <span class="tag">${(+u.headcount||1)} чел.</span>${u.reports_to&&u.reports_to!=='—'?` <span class="muted">↑ ${esc(u.reports_to)}</span>`:''}${(u.owns_functions&&u.owns_functions.length)?`<div class="muted">Отвечает: ${u.owns_functions.map(esc).join(", ")}</div>`:''}${u.note?`<div class="muted">⚠ ${esc(u.note)}</div>`:''}</div>`});}
|
if(state.orgchart&&(state.orgchart.units||[]).length){body+=H("Оргструктура");body+=`<div class="item muted">${esc(state.orgchart.insight||'')}</div>`;state.orgchart.units.forEach(u=>{body+=`<div class="item"><b>${esc(u.role)}</b> <span class="tag">${(+u.headcount||1)} чел.</span>${u.reports_to&&u.reports_to!=='—'?` <span class="muted">↑ ${esc(u.reports_to)}</span>`:''}${(u.owns_functions&&u.owns_functions.length)?`<div class="muted">Отвечает: ${u.owns_functions.map(esc).join(", ")}</div>`:''}${u.note?`<div class="muted">${ic('alert',12)} ${esc(u.note)}</div>`:''}</div>`});}
|
||||||
// Должностные инструкции
|
// Должностные инструкции
|
||||||
if(state.jobs&&(state.jobs.roles||[]).length){body+=H("Должностные инструкции");state.jobs.roles.forEach(r=>{body+=`<div class="item"><b>${esc(r.role)}</b><div class="muted">${esc(r.purpose||'')}${r.reports_to?` · ↑ ${esc(r.reports_to)}`:''}</div>${(r.responsibilities&&r.responsibilities.length)?`<div style="margin-top:5px"><b>Ответственность:</b><ul>${r.responsibilities.map(x=>`<li>${esc(x)}</li>`).join("")}</ul></div>`:''}${(r.kpis&&r.kpis.length)?`<div><b>KPI:</b> ${r.kpis.map(esc).join(" · ")}</div>`:''}${(r.authority&&r.authority.length)?`<div class="muted">Полномочия: ${r.authority.map(esc).join(" · ")}</div>`:''}${r.deviation_note?`<div class="muted">⚠ Учтено пожелание клиента: ${esc(r.deviation_note)}</div>`:''}</div>`});}
|
if(state.jobs&&(state.jobs.roles||[]).length){body+=H("Должностные инструкции");state.jobs.roles.forEach(r=>{body+=`<div class="item"><b>${esc(r.role)}</b><div class="muted">${esc(r.purpose||'')}${r.reports_to?` · ↑ ${esc(r.reports_to)}`:''}</div>${(r.responsibilities&&r.responsibilities.length)?`<div style="margin-top:5px"><b>Ответственность:</b><ul>${r.responsibilities.map(x=>`<li>${esc(x)}</li>`).join("")}</ul></div>`:''}${(r.kpis&&r.kpis.length)?`<div><b>KPI:</b> ${r.kpis.map(esc).join(" · ")}</div>`:''}${(r.authority&&r.authority.length)?`<div class="muted">Полномочия: ${r.authority.map(esc).join(" · ")}</div>`:''}${r.deviation_note?`<div class="muted">${ic('alert',12)} Учтено пожелание клиента: ${esc(r.deviation_note)}</div>`:''}</div>`});}
|
||||||
if(s.open_questions&&s.open_questions.length){body+=H("Уточнить перед разработкой")+`<ul>${s.open_questions.map(q=>`<li>${esc(q)}</li>`).join("")}</ul>`}
|
if(s.open_questions&&s.open_questions.length){body+=H("Уточнить перед разработкой")+`<ul>${s.open_questions.map(q=>`<li>${esc(q)}</li>`).join("")}</ul>`}
|
||||||
w.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>ТЗ — ${cn}</title><style>
|
w.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>ТЗ — ${cn}</title><style>
|
||||||
@page{margin:18mm}body{font-family:'Segoe UI',Arial,sans-serif;color:#1A1A2E;line-height:1.5;max-width:780px;margin:0 auto;padding:20px}
|
@page{margin:18mm}body{font-family:'Segoe UI',Arial,sans-serif;color:#1A1A2E;line-height:1.5;max-width:780px;margin:0 auto;padding:20px}
|
||||||
@ -1246,7 +1258,7 @@ function renderOrgChart(o){
|
|||||||
${u.reports_to&&u.reports_to!=='—'?`<span style="font-size:11px;color:#9ca3af">↑ ${esc(u.reports_to)}</span>`:'<span style="font-size:10px;font-weight:700;color:#6366F1;background:#EEF2FF;padding:1px 8px;border-radius:5px">руководство</span>'}
|
${u.reports_to&&u.reports_to!=='—'?`<span style="font-size:11px;color:#9ca3af">↑ ${esc(u.reports_to)}</span>`:'<span style="font-size:10px;font-weight:700;color:#6366F1;background:#EEF2FF;padding:1px 8px;border-radius:5px">руководство</span>'}
|
||||||
</div>
|
</div>
|
||||||
${(u.owns_functions&&u.owns_functions.length)?`<div style="font-size:11px;color:#6b7280;margin-top:6px">Отвечает: ${u.owns_functions.map(f=>`<span style="background:#F1F5F9;padding:1px 6px;border-radius:4px;margin-right:3px;display:inline-block">${esc(f)}</span>`).join('')}</div>`:''}
|
${(u.owns_functions&&u.owns_functions.length)?`<div style="font-size:11px;color:#6b7280;margin-top:6px">Отвечает: ${u.owns_functions.map(f=>`<span style="background:#F1F5F9;padding:1px 6px;border-radius:4px;margin-right:3px;display:inline-block">${esc(f)}</span>`).join('')}</div>`:''}
|
||||||
${u.note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:6px">⚠ ${esc(u.note)}</div>`:''}
|
${u.note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:6px">${ic('alert',12)} ${esc(u.note)}</div>`:''}
|
||||||
</div>`).join('');
|
</div>`).join('');
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
@ -1258,7 +1270,7 @@ function renderJobs(j){
|
|||||||
${(r.responsibilities&&r.responsibilities.length)?`<div style="font-size:11px;font-weight:700;color:#374151;margin-bottom:3px">Ответственность</div><ul style="margin:0 0 9px;padding-left:18px;font-size:12px;color:#4B5563">${r.responsibilities.map(x=>`<li>${esc(x)}</li>`).join('')}</ul>`:''}
|
${(r.responsibilities&&r.responsibilities.length)?`<div style="font-size:11px;font-weight:700;color:#374151;margin-bottom:3px">Ответственность</div><ul style="margin:0 0 9px;padding-left:18px;font-size:12px;color:#4B5563">${r.responsibilities.map(x=>`<li>${esc(x)}</li>`).join('')}</ul>`:''}
|
||||||
${(r.kpis&&r.kpis.length)?`<div style="font-size:11px;font-weight:700;color:#047857;margin-bottom:3px">KPI</div><div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:9px">${r.kpis.map(k=>`<span style="font-size:11px;background:#ECFDF5;color:#047857;padding:2px 8px;border-radius:5px">${esc(k)}</span>`).join('')}</div>`:''}
|
${(r.kpis&&r.kpis.length)?`<div style="font-size:11px;font-weight:700;color:#047857;margin-bottom:3px">KPI</div><div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:9px">${r.kpis.map(k=>`<span style="font-size:11px;background:#ECFDF5;color:#047857;padding:2px 8px;border-radius:5px">${esc(k)}</span>`).join('')}</div>`:''}
|
||||||
${(r.authority&&r.authority.length)?`<div style="font-size:11px;color:#6b7280">Полномочия: ${r.authority.map(a=>esc(a)).join(' · ')}</div>`:''}
|
${(r.authority&&r.authority.length)?`<div style="font-size:11px;color:#6b7280">Полномочия: ${r.authority.map(a=>esc(a)).join(' · ')}</div>`:''}
|
||||||
${r.deviation_note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:8px">⚠ Учтено пожелание клиента: ${esc(r.deviation_note)}</div>`:''}
|
${r.deviation_note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:8px">${ic('alert',12)} Учтено пожелание клиента: ${esc(r.deviation_note)}</div>`:''}
|
||||||
</div>`).join('');
|
</div>`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1267,12 +1279,12 @@ function approved(s){return state.approvals&&state.approvals[s]}
|
|||||||
async function approve(s){await fetch(`${API}/api/project/approve`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage:s,approved:true})});state.approvals=state.approvals||{};state.approvals[s]=1;renderClient();}
|
async function approve(s){await fetch(`${API}/api/project/approve`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage:s,approved:true})});state.approvals=state.approvals||{};state.approvals[s]=1;renderClient();}
|
||||||
async function unapprove(s){await fetch(`${API}/api/project/approve`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage:s,approved:false})});if(state.approvals)delete state.approvals[s];renderClient();}
|
async function unapprove(s){await fetch(`${API}/api/project/approve`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage:s,approved:false})});if(state.approvals)delete state.approvals[s];renderClient();}
|
||||||
function setTab(t){activeTab=t;renderClient();}
|
function setTab(t){activeTab=t;renderClient();}
|
||||||
function cpBar(s,l){if(approved(s))return `<div class="cpbar appr"><div class="cpbar-t">${l}</div><span style="color:var(--primary);font-weight:700;font-size:13px">✓ Утверждено</span><button class="cp-btn cp-r" onclick="unapprove('${s}')">Снять</button></div>`;return `<div class="cpbar"><div class="cpbar-t">✋ ${l}</div><button class="cp-btn cp-r" onclick="rerun('${s}')">Перестроить</button><button class="cp-btn cp-a" onclick="approve('${s}')">Утвердить →</button></div>`;}
|
function cpBar(s,l){if(approved(s))return `<div class="cpbar appr"><div class="cpbar-t">${l}</div><span style="color:var(--primary);font-weight:700;font-size:13px;display:inline-flex;align-items:center;gap:5px">${ic('check',13)} Утверждено</span><button class="cp-btn cp-r" onclick="unapprove('${s}')">Снять</button></div>`;return `<div class="cpbar"><div class="cpbar-t" style="display:flex;align-items:center;gap:7px">${ic('alert',14)} ${l}</div><button class="cp-btn cp-r" onclick="rerun('${s}')">Перестроить</button><button class="cp-btn cp-a" onclick="approve('${s}')">Утвердить →</button></div>`;}
|
||||||
function runCard(s,ic,t,d,b){return `<div class="run-card"><div class="run-ic">${ic}</div><div class="run-t">${t}</div><div class="run-d">${d}</div><button class="run-btn" id="rb-${s}" onclick="rerun('${s}')">${b}</button></div>`;}
|
function runCard(s,ic,t,d,b){return `<div class="run-card"><div class="run-ic">${ic}</div><div class="run-t">${t}</div><div class="run-d">${d}</div><button class="run-btn" id="rb-${s}" onclick="rerun('${s}')">${b}</button></div>`;}
|
||||||
|
|
||||||
function renderTab(){const c=document.getElementById("tabContent");
|
function renderTab(){const c=document.getElementById("tabContent");
|
||||||
if(activeTab==="interview"){c.innerHTML=`<div style="max-width:720px">${state.messages.map(m=>`<div style="display:flex;gap:9px;margin-bottom:12px;${m.role==='user'?'flex-direction:row-reverse':''}"><div class="cl-av" style="border-radius:50%;background:${m.role==='user'?'#6366F1':'#047857'}">${m.role==='user'?'К':'Е'}</div><div style="padding:10px 14px;border-radius:13px;font-size:13px;line-height:1.5;max-width:78%;white-space:pre-wrap;${m.role==='user'?'background:#047857;color:#fff':'background:#fff;border:1px solid #E5E7EB'}">${fmt(m.content)}</div></div>`).join("")||'<div class="empty" style="height:200px">Интервью не начато</div>'}</div>`;}
|
if(activeTab==="interview"){c.innerHTML=`<div style="max-width:720px">${state.messages.map(m=>`<div style="display:flex;gap:9px;margin-bottom:12px;${m.role==='user'?'flex-direction:row-reverse':''}"><div class="cl-av" style="border-radius:50%;background:${m.role==='user'?'#6366F1':'#047857'}">${m.role==='user'?'К':'Е'}</div><div style="padding:10px 14px;border-radius:13px;font-size:13px;line-height:1.5;max-width:78%;white-space:pre-wrap;${m.role==='user'?'background:#047857;color:#fff':'background:#fff;border:1px solid #E5E7EB'}">${fmt(m.content)}</div></div>`).join("")||'<div class="empty" style="height:200px">Интервью не начато</div>'}</div>`;}
|
||||||
else if(activeTab==="methods"){if(!state.selection){c.innerHTML=runCard("methods","🎯","Подбор методологий","Елена предложит набор методологий под тип бизнеса.","Подобрать →");return}const s=state.selection;c.innerHTML=`<div class="blk"><div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;margin-bottom:4px">Тип бизнеса</div><div style="font-size:16px;font-weight:700;margin-bottom:12px">${esc(s.business_type)}</div>${s.recommended.map(r=>`<div style="display:flex;gap:10px;padding:8px 0;border-top:1px solid #f1f5f9"><span style="font-size:16px">${r.use?'✅':'⬜'}</span><div style="flex:1"><b>${r.method.toUpperCase()}</b> <span style="font-size:11px;color:#9ca3af">[${r.depth}]</span><div style="font-size:12px;color:var(--muted)">${esc(r.reason)}</div></div></div>`).join("")}<div style="margin-top:12px;padding:10px;background:var(--light);border-radius:8px;font-size:13px">${esc(s.rationale)}</div></div>${cpBar("methods","Согласны с набором?")}`;}
|
else if(activeTab==="methods"){if(!state.selection){c.innerHTML=runCard("methods",ic('target',30),"Подбор методологий","Елена предложит набор методологий под тип бизнеса.","Подобрать →");return}const s=state.selection;c.innerHTML=`<div class="blk"><div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;margin-bottom:4px">Тип бизнеса</div><div style="font-size:16px;font-weight:700;margin-bottom:12px">${esc(s.business_type)}</div>${s.recommended.map(r=>`<div style="display:flex;gap:10px;padding:8px 0;border-top:1px solid #f1f5f9"><span style="color:${r.use?'#047857':'#CBD5E1'};display:inline-flex">${r.use?ic('checkCircle',16):ic('dot',16)}</span><div style="flex:1"><b>${r.method.toUpperCase()}</b> <span style="font-size:11px;color:#9ca3af">[${r.depth}]</span><div style="font-size:12px;color:var(--muted)">${esc(r.reason)}</div></div></div>`).join("")}<div style="margin-top:12px;padding:10px;background:var(--light);border-radius:8px;font-size:13px">${esc(s.rationale)}</div></div>${cpBar("methods","Согласны с набором?")}`;}
|
||||||
else if(activeTab==="canvas"){if(!state.canvas){c.innerHTML=runCard("canvas",ic('chart',30),"Business Model Canvas","Стратегия — 9 блоков.","Построить →");return}c.innerHTML=renderCanvas(state.canvas)+cpBar("canvas","Стратегия верна?");}
|
else if(activeTab==="canvas"){if(!state.canvas){c.innerHTML=runCard("canvas",ic('chart',30),"Business Model Canvas","Стратегия — 9 блоков.","Построить →");return}c.innerHTML=renderCanvas(state.canvas)+cpBar("canvas","Стратегия верна?");}
|
||||||
else if(activeTab==="idef0"){if(!state.model){c.innerHTML=runCard("model",ic('process',30),"Функциональная модель IDEF0","Функции, входы/выходы, нормы, разрывы.","Построить →");return}c.innerHTML=renderIdef(state.model)+cpBar("idef0","Модель верна?");}
|
else if(activeTab==="idef0"){if(!state.model){c.innerHTML=runCard("model",ic('process',30),"Функциональная модель IDEF0","Функции, входы/выходы, нормы, разрывы.","Построить →");return}c.innerHTML=renderIdef(state.model)+cpBar("idef0","Модель верна?");}
|
||||||
else if(activeTab==="org"){
|
else if(activeTab==="org"){
|
||||||
@ -1296,21 +1308,21 @@ function renderSpecTab(){
|
|||||||
let head=`<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap">`;
|
let head=`<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap">`;
|
||||||
if(devN>0||hasClient){
|
if(devN>0||hasClient){
|
||||||
head+=`<div style="display:flex;gap:3px;background:#F1F5F9;border-radius:9px;padding:3px">
|
head+=`<div style="display:flex;gap:3px;background:#F1F5F9;border-radius:9px;padding:3px">
|
||||||
<button onclick="setSpecVariant('elena')" style="padding:6px 12px;border:none;border-radius:7px;cursor:pointer;font-size:12px;font-weight:700;font-family:Inter;${specVariant==='elena'?'background:#047857;color:#fff':'background:transparent;color:#6B7280'}">📐 Эталон Елены</button>
|
<button onclick="setSpecVariant('elena')" style="padding:6px 12px;border:none;border-radius:7px;cursor:pointer;font-size:12px;font-weight:700;font-family:Inter;${specVariant==='elena'?'background:#047857;color:#fff':'background:transparent;color:#6B7280'}">${ic('ruler',14)} Эталон Елены</button>
|
||||||
<button onclick="setSpecVariant('client')" style="padding:6px 12px;border:none;border-radius:7px;cursor:pointer;font-size:12px;font-weight:700;font-family:Inter;${specVariant==='client'?'background:#92400E;color:#fff':'background:transparent;color:#6B7280'}">⚠️ Вариант клиента${hasClient?'':' (нет)'}</button>
|
<button onclick="setSpecVariant('client')" style="padding:6px 12px;border:none;border-radius:7px;cursor:pointer;font-size:12px;font-weight:700;font-family:Inter;${specVariant==='client'?'background:#92400E;color:#fff':'background:transparent;color:#6B7280'}">${ic('alert',14)} Вариант клиента${hasClient?'':' (нет)'}</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
head+=`<button class="btn btn-p" style="margin-left:auto" onclick="exportSpecPDF()">⬇ Скачать ТЗ (PDF)</button></div>`;
|
head+=`<button class="btn btn-p" style="margin-left:auto" onclick="exportSpecPDF()">${ic('download',16)} Скачать ТЗ (PDF)</button></div>`;
|
||||||
// Кнопка пересборки под вариант клиента (Phase 3) — только если есть отклонения
|
// Кнопка пересборки под вариант клиента (Phase 3) — только если есть отклонения
|
||||||
let phase3='';
|
let phase3='';
|
||||||
if(devN>0){
|
if(devN>0){
|
||||||
phase3=`<div style="background:#FFFBEB;border:1px solid #FDE68A;border-radius:10px;padding:11px 14px;margin-bottom:14px;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
phase3=`<div style="background:#FFFBEB;border:1px solid #FDE68A;border-radius:10px;padding:11px 14px;margin-bottom:14px;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||||
<span style="font-size:18px">⚠️</span>
|
<span style="color:#92400E;display:inline-flex">${ic('alert',18)}</span>
|
||||||
<div style="flex:1;min-width:160px;font-size:12px;color:#92400E">Зафиксировано отклонений: <b>${devN}</b>. ${hasClient?'Вариант клиента собран — переключай выше.':'Можно собрать отдельное ТЗ под реальный вариант клиента.'}</div>
|
<div style="flex:1;min-width:160px;font-size:12px;color:#92400E">Зафиксировано отклонений: <b>${devN}</b>. ${hasClient?'Вариант клиента собран — переключай выше.':'Можно собрать отдельное ТЗ под реальный вариант клиента.'}</div>
|
||||||
<button class="run-btn" id="rb-specclient" onclick="buildSpecClient()" style="font-size:12px;background:#92400E">${hasClient?'↻ Пересобрать вариант клиента':'🔧 Собрать ТЗ под вариант клиента'}</button>
|
<button class="run-btn" id="rb-specclient" onclick="buildSpecClient()" style="font-size:12px;background:#92400E">${hasClient?'↻ Пересобрать вариант клиента':ic('wrench',13)+' Собрать ТЗ под вариант клиента'}</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
const banner=showClient?`<div style="background:#FEF3C7;color:#92400E;font-size:12px;font-weight:700;padding:8px 12px;border-radius:8px;margin-bottom:12px">⚠️ Вариант клиента — реализуем то, на чём настоял клиент. Риски вынесены в «Уточнить перед разработкой».</div>`:'';
|
const banner=showClient?`<div style="background:#FEF3C7;color:#92400E;font-size:12px;font-weight:700;padding:8px 12px;border-radius:8px;margin-bottom:12px;display:flex;align-items:center;gap:7px">${ic('alert',14)} Вариант клиента — реализуем то, на чём настоял клиент. Риски вынесены в «Уточнить перед разработкой».</div>`:'';
|
||||||
return head+phase3+banner+renderSpec(s)+cpBar("spec","ТЗ готово к разработке?");
|
return head+phase3+banner+renderSpec(s)+cpBar("spec","ТЗ готово к разработке?");
|
||||||
}
|
}
|
||||||
function setSpecVariant(v){specVariant=v;renderTab();}
|
function setSpecVariant(v){specVariant=v;renderTab();}
|
||||||
@ -1327,7 +1339,7 @@ async function rerun(stage){const [ep,key]=BUILD[stage];const btn=document.getEl
|
|||||||
|
|
||||||
function renderCanvas(c){const B=(k,cls,l)=>{const b=c[k];return `<div class="cv ${cls}"><div class="cv-h">${l}<span>${b.completeness}%</span></div><ul>${b.items.map(i=>`<li>${esc(i)}</li>`).join("")}</ul></div>`};return `<div class="canvas-grid">${B("key_partners","kp","Партнёры")}${B("key_activities","ka","Активности")}${B("value_propositions","vp","Ценность")}${B("customer_relationships","cr","Отношения")}${B("customer_segments","cs","Сегменты")}${B("key_resources","kr","Ресурсы")}${B("channels","ch","Каналы")}${B("cost_structure","co","Издержки")}${B("revenue_streams","rev","Доходы")}<div class="cv-ins"><b>Вывод:</b> ${esc(c.insight)}</div></div>`;}
|
function renderCanvas(c){const B=(k,cls,l)=>{const b=c[k];return `<div class="cv ${cls}"><div class="cv-h">${l}<span>${b.completeness}%</span></div><ul>${b.items.map(i=>`<li>${esc(i)}</li>`).join("")}</ul></div>`};return `<div class="canvas-grid">${B("key_partners","kp","Партнёры")}${B("key_activities","ka","Активности")}${B("value_propositions","vp","Ценность")}${B("customer_relationships","cr","Отношения")}${B("customer_segments","cs","Сегменты")}${B("key_resources","kr","Ресурсы")}${B("channels","ch","Каналы")}${B("cost_structure","co","Издержки")}${B("revenue_streams","rev","Доходы")}<div class="cv-ins"><b>Вывод:</b> ${esc(c.insight)}</div></div>`;}
|
||||||
function renderIdef(m){const box=(fn,ct,ins,outs,me,id,pct)=>{const C=(ct&&ct.length)?ct.map(c=>`<span class="ar">${esc(c.name)}</span>`).join(""):`<span class="ar nomiss">нет управления</span>`;const I=(ins||[]).map(a=>`<span class="ar">${esc(a.name)}</span>`).join("")||'<span class="ar">—</span>';const O=(outs||[]).map(a=>`<span class="ar ${a.target==='НИКУДА'?'dead':''}">${esc(a.name)}${a.target==='НИКУДА'?' ⊘':''}</span>`).join("")||'<span class="ar">—</span>';const M=(me||[]).map(x=>`<span class="ar">${esc(x.name)}</span>`).join("")||'<span class="ar">—</span>';return `<div class="idef"><div class="idef-c">${C}</div><div class="idef-mid"><div class="idef-i">${I}</div><div class="idef-fn">${id?`<b>${id}</b>`:''}${esc(fn)}${pct!=null?`<i style="color:${pct>=70?'#047857':pct>=45?'#F59E0B':'#EF4444'}">${pct}%</i>`:''}</div><div class="idef-o">${O}</div></div><div class="idef-m">${M}</div></div>`};let h=`<div style="background:var(--ink);color:#fff;border-radius:10px;padding:11px 15px;margin-bottom:12px;font-size:13px"><b style="color:var(--mid)">Паттерн:</b> ${esc(m.business_pattern)}</div>`;if(m.context)h+=`<div class="idef-lbl">A-0 Контекст</div>`+box(m.context.function,m.context.controls,m.context.inputs,m.context.outputs,m.context.mechanisms,"A0");h+=`<div class="idef-lbl">Декомпозиция · ${m.activities.length}</div>`;m.activities.forEach(a=>h+=box(a.function,a.controls,a.inputs,a.outputs,a.mechanisms,a.node_id,a.completeness));if(m.arrow_issues&&m.arrow_issues.length){h+=`<div class="idef-lbl">Разрывы · ${m.arrow_issues.length}</div>`;m.arrow_issues.forEach(g=>{const col=g.severity==='critical'?'#DC2626':g.severity==='high'?'#92400E':'#1E40AF';h+=`<div class="blk" style="border-left:3px solid ${col};padding:10px 13px"><div style="font-size:10px;font-weight:700;color:#9ca3af">${esc(g.node_id)} · ${g.type}</div><div style="font-size:13px;font-weight:700">${esc(g.title)}</div><div style="font-size:12px;color:#6b7280;margin-top:3px">${esc(g.description)}</div></div>`})}return h;}
|
function renderIdef(m){const box=(fn,ct,ins,outs,me,id,pct)=>{const C=(ct&&ct.length)?ct.map(c=>`<span class="ar">${esc(c.name)}</span>`).join(""):`<span class="ar nomiss">нет управления</span>`;const I=(ins||[]).map(a=>`<span class="ar">${esc(a.name)}</span>`).join("")||'<span class="ar">—</span>';const O=(outs||[]).map(a=>`<span class="ar ${a.target==='НИКУДА'?'dead':''}">${esc(a.name)}${a.target==='НИКУДА'?' ⊘':''}</span>`).join("")||'<span class="ar">—</span>';const M=(me||[]).map(x=>`<span class="ar">${esc(x.name)}</span>`).join("")||'<span class="ar">—</span>';return `<div class="idef"><div class="idef-c">${C}</div><div class="idef-mid"><div class="idef-i">${I}</div><div class="idef-fn">${id?`<b>${id}</b>`:''}${esc(fn)}${pct!=null?`<i style="color:${pct>=70?'#047857':pct>=45?'#F59E0B':'#EF4444'}">${pct}%</i>`:''}</div><div class="idef-o">${O}</div></div><div class="idef-m">${M}</div></div>`};let h=`<div style="background:var(--ink);color:#fff;border-radius:10px;padding:11px 15px;margin-bottom:12px;font-size:13px"><b style="color:var(--mid)">Паттерн:</b> ${esc(m.business_pattern)}</div>`;if(m.context)h+=`<div class="idef-lbl">A-0 Контекст</div>`+box(m.context.function,m.context.controls,m.context.inputs,m.context.outputs,m.context.mechanisms,"A0");h+=`<div class="idef-lbl">Декомпозиция · ${m.activities.length}</div>`;m.activities.forEach(a=>h+=box(a.function,a.controls,a.inputs,a.outputs,a.mechanisms,a.node_id,a.completeness));if(m.arrow_issues&&m.arrow_issues.length){h+=`<div class="idef-lbl">Разрывы · ${m.arrow_issues.length}</div>`;m.arrow_issues.forEach(g=>{const col=g.severity==='critical'?'#DC2626':g.severity==='high'?'#92400E':'#1E40AF';h+=`<div class="blk" style="border-left:3px solid ${col};padding:10px 13px"><div style="font-size:10px;font-weight:700;color:#9ca3af">${esc(g.node_id)} · ${g.type}</div><div style="font-size:13px;font-weight:700">${esc(g.title)}</div><div style="font-size:12px;color:#6b7280;margin-top:3px">${esc(g.description)}</div></div>`})}return h;}
|
||||||
function renderSpec(s){let h=`<div class="spec-h"><span class="pl">A</span>Обзор</div><div class="blk">${esc(s.overview)}</div><div class="spec-h"><span class="pl">A</span>Роли (${s.roles.length})</div>`;s.roles.forEach(r=>h+=`<div class="blk" style="padding:11px 14px"><b>${esc(r.name)}</b> — ${esc(r.does)}<div style="font-size:11px;color:#9ca3af;margin-top:3px">Доступ: ${esc(r.access)}</div></div>`);h+=`<div class="spec-h"><span class="pl">B</span>Модули (${s.modules.length})</div>`;s.modules.forEach((m,mi)=>h+=`<div class="mod"><div style="display:flex;align-items:center;gap:8px;margin-bottom:5px"><span class="mod-node">${esc(m.source_node)}</span><b>${esc(m.name)}</b><button class="cp-btn cp-r" style="margin-left:auto;padding:5px 11px" onclick="designScreen('${esc(m.name).replace(/'/g,"")}',${mi})">🖼 Экран</button></div><div style="font-size:12px;color:var(--muted)">${esc(m.purpose)}</div><div style="margin:5px 0">${m.screens.map(x=>`<span class="scr">${esc(x)}</span>`).join("")}</div><div style="font-size:12px;color:#374151">📥 ${esc(m.inputs_data)} · 📤 ${esc(m.outputs_data)}</div>${m.rules.length?`<div style="margin-top:5px">${m.rules.map(r=>`<div class="blk-pain">${esc(r)}</div>`).join("")}</div>`:''}<div id="screen-${mi}" style="margin-top:10px"></div></div>`);h+=`<div class="spec-h"><span class="pl">C</span>Данные (${s.entities.length} таблиц)</div>`;s.entities.forEach(e=>h+=`<div class="ent"><b style="font-family:Montserrat">◆ ${esc(e.name)}</b><div class="ent-fields" style="margin-top:7px">${e.fields.map(f=>`<div class="fld"><b>${esc(f.field)}</b> <em>${esc(f.type)}</em></div>`).join("")}</div>${e.relations.length?`<div style="font-size:11px;color:#6b7280;margin-bottom:5px">🔗 ${e.relations.map(esc).join(" · ")}</div>`:''}<div class="ent-ex">${esc(e.example)}</div></div>`);if(s.open_questions&&s.open_questions.length){h+=`<div class="blk" style="border-color:#FDE68A;background:#FFFBEB"><b>Уточнить перед разработкой</b>${s.open_questions.map(q=>`<div class="blk-pain" style="margin-top:6px">${esc(q)}</div>`).join("")}</div>`}return h;}
|
function renderSpec(s){let h=`<div class="spec-h"><span class="pl">A</span>Обзор</div><div class="blk">${esc(s.overview)}</div><div class="spec-h"><span class="pl">A</span>Роли (${s.roles.length})</div>`;s.roles.forEach(r=>h+=`<div class="blk" style="padding:11px 14px"><b>${esc(r.name)}</b> — ${esc(r.does)}<div style="font-size:11px;color:#9ca3af;margin-top:3px">Доступ: ${esc(r.access)}</div></div>`);h+=`<div class="spec-h"><span class="pl">B</span>Модули (${s.modules.length})</div>`;s.modules.forEach((m,mi)=>h+=`<div class="mod"><div style="display:flex;align-items:center;gap:8px;margin-bottom:5px"><span class="mod-node">${esc(m.source_node)}</span><b>${esc(m.name)}</b><button class="cp-btn cp-r" style="margin-left:auto;padding:5px 11px" onclick="designScreen('${esc(m.name).replace(/'/g,"")}',${mi})">${ic('screen',13)} Экран</button></div><div style="font-size:12px;color:var(--muted)">${esc(m.purpose)}</div><div style="margin:5px 0">${m.screens.map(x=>`<span class="scr">${esc(x)}</span>`).join("")}</div><div style="font-size:12px;color:#374151">Вход: ${esc(m.inputs_data)} · Выход: ${esc(m.outputs_data)}</div>${m.rules.length?`<div style="margin-top:5px">${m.rules.map(r=>`<div class="blk-pain">${esc(r)}</div>`).join("")}</div>`:''}<div id="screen-${mi}" style="margin-top:10px"></div></div>`);h+=`<div class="spec-h"><span class="pl">C</span>Данные (${s.entities.length} таблиц)</div>`;s.entities.forEach(e=>h+=`<div class="ent"><b style="font-family:Montserrat">◆ ${esc(e.name)}</b><div class="ent-fields" style="margin-top:7px">${e.fields.map(f=>`<div class="fld"><b>${esc(f.field)}</b> <em>${esc(f.type)}</em></div>`).join("")}</div>${e.relations.length?`<div style="font-size:11px;color:#6b7280;margin-bottom:5px">Связи: ${e.relations.map(esc).join(" · ")}</div>`:''}<div class="ent-ex">${esc(e.example)}</div></div>`);if(s.open_questions&&s.open_questions.length){h+=`<div class="blk" style="border-color:#FDE68A;background:#FFFBEB"><b>Уточнить перед разработкой</b>${s.open_questions.map(q=>`<div class="blk-pain" style="margin-top:6px">${esc(q)}</div>`).join("")}</div>`}return h;}
|
||||||
|
|
||||||
async function checkAiStatus(){
|
async function checkAiStatus(){
|
||||||
try{
|
try{
|
||||||
@ -1336,8 +1348,8 @@ async function checkAiStatus(){
|
|||||||
if(d.ok){b.style.display='none';if(dot){dot.style.background='#10B981';dot.title='AI-движок Елены: в норме';}return;}
|
if(d.ok){b.style.display='none';if(dot){dot.style.background='#10B981';dot.title='AI-движок Елены: в норме';}return;}
|
||||||
if(dot){dot.style.background='#EF4444';dot.title='AI-движок Елены: недоступен';}
|
if(dot){dot.style.background='#EF4444';dot.title='AI-движок Елены: недоступен';}
|
||||||
t.textContent=d.reason==='low_balance'
|
t.textContent=d.reason==='low_balance'
|
||||||
?'⚠️ AI-движок Елены недоступен: недостаточно средств на балансе. Пополните баланс — без него Елена не строит стратегию, модель, оргструктуру, должностные и ТЗ.'
|
?'AI-движок Елены недоступен: недостаточно средств на балансе. Пополните баланс — без него Елена не строит стратегию, модель, оргструктуру, должностные и ТЗ.'
|
||||||
:'⚠️ AI-движок Елены временно недоступен (техническая ошибка). Генерация артефактов не работает.';
|
:'AI-движок Елены временно недоступен (техническая ошибка). Генерация артефактов не работает.';
|
||||||
b.style.display='flex';
|
b.style.display='flex';
|
||||||
}catch(e){/* сетевой сбой — не показываем ложную тревогу */}
|
}catch(e){/* сетевой сбой — не показываем ложную тревогу */}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user