wasrusgen1-crm/docs/crm.html
2026-06-01 07:55:10 +03:00

776 lines
80 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CRM консультанта · @wasrusgen1 | КОНСАЛТИНГ</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Montserrat:wght@700;800&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--primary:#047857;--dark:#064E3B;--mid:#10B981;--light:#ECFDF5;--ink:#0F0F1A;--bg:#F5F6F8;--white:#fff;--border:#E5E7EB;--text:#1A1A2E;--muted:#6B7280}
html,body{height:100%;overflow:hidden}
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);display:flex;flex-direction:column}
.hdr{height:54px;background:var(--dark);display:flex;align-items:center;padding:0 20px;gap:12px;flex-shrink:0;z-index:10}
.hdr-ic{width:30px;height:30px;background:var(--primary);border-radius:8px;display:flex;align-items:center;justify-content:center;font-family:'Montserrat';font-weight:800;color:#fff;font-size:16px}
.hdr-t{font-family:'Montserrat';font-weight:700;font-size:14px;color:rgba(255,255,255,.6);display:flex;align-items:center;gap:9px;letter-spacing:-.2px}
.hdr-sep{width:1.5px;height:15px;background:rgba(255,255,255,.25);flex-shrink:0}
.hdr-t b{font-weight:800;color:#fff}
.hdr-badge{background:rgba(16,185,129,.15);border:1px solid rgba(16,185,129,.25);color:var(--mid);font-size:10px;font-weight:700;letter-spacing:.05em;border-radius:6px;padding:2px 8px}
.hdr-r{margin-left:auto;display:flex;align-items:center;gap:8px;color:rgba(255,255,255,.6);font-size:13px}
.layout{flex:1;display:flex;overflow:hidden}
.sb{width:240px;flex-shrink:0;background:var(--ink);display:flex;flex-direction:column;overflow:hidden}
.sb-nav{padding:12px 10px}
.nav-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:9px;cursor:pointer;color:rgba(255,255,255,.6);font-size:13px;font-weight:600;margin-bottom:3px}
.nav-item:hover{background:rgba(255,255,255,.05);color:#fff}
.nav-item.active{background:rgba(4,120,87,.2);color:#fff}
.nav-item .ic{font-size:16px}
.sb-cap{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.25);padding:14px 14px 6px}
.sb-list{flex:1;overflow-y:auto;padding:0 10px}
.cl{padding:10px;border-radius:9px;cursor:pointer;margin-bottom:3px;display:flex;align-items:center;gap:9px}
.cl:hover{background:rgba(255,255,255,.04)}
.cl.active{background:rgba(4,120,87,.18)}
.cl-av{width:28px;height:28px;border-radius:7px;background:#6366F1;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:11px;color:#fff;flex-shrink:0}
.cl-n{font-size:12px;font-weight:600;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
.cl-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.sb-new{margin:8px 10px;display:flex;align-items:center;justify-content:center;gap:7px;padding:10px;border-radius:9px;background:var(--primary);color:#fff;border:none;cursor:pointer;font-family:'Inter';font-weight:700;font-size:13px}
.main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
.scroll{flex:1;overflow-y:auto;padding:24px 28px}
/* Dashboard */
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px}
.kpi{background:var(--white);border:1.5px solid var(--border);border-radius:10px;padding:13px 16px}
.kpi-v{font-family:'Montserrat';font-weight:800;font-size:23px;color:var(--ink);letter-spacing:-.5px;line-height:1.1}
.kpi-l{font-size:11px;color:var(--muted);margin-top:3px}
.kpi-sub{font-size:10.5px;color:var(--primary);font-weight:600;margin-top:4px}
.sec-h{font-family:'Montserrat';font-weight:800;font-size:15px;color:var(--ink);margin-bottom:9px;margin-top:6px}
.cl-row{display:flex;align-items:center;gap:12px;cursor:pointer;padding:10px 14px;border-top:1px solid var(--bg)}
.cl-row:first-child{border-top:none}
.cl-row:hover{background:#FAFBFC}
.mini-chip{font-size:10px;font-weight:700;padding:2px 8px;border-radius:6px;white-space:nowrap;display:inline-block}
.tbl{background:var(--white);border:1px solid var(--border);border-radius:12px;padding:0;margin-bottom:14px;overflow:hidden}
.tbl-row{display:flex;align-items:center;gap:12px;cursor:pointer;padding:9px 14px;border-top:1px solid var(--bg)}
.tbl-row:first-child{border-top:none}
.tbl-row:hover{background:#FAFBFC}
.fchip{font-size:11px;font-weight:600;font-family:'Inter';padding:4px 11px;border-radius:7px;border:1px solid var(--border);background:var(--white);color:var(--muted);cursor:pointer;margin-left:6px}
.fchip:hover{border-color:var(--primary);color:var(--primary)}
.fchip.on{background:var(--primary);color:#fff;border-color:var(--primary)}
.sec-h.collapsible{display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none}
.sec-chev{display:inline-block;transition:transform .15s;font-size:11px;color:var(--muted)}
/* Pipeline kanban */
.kanban{display:flex;gap:12px;overflow-x:auto;padding-bottom:8px;align-items:flex-start}
.kcol{flex:1;min-width:200px;background:#eef0f3;border-radius:12px;padding:10px}
.kcol-h{font-size:12px;font-weight:700;color:var(--text);padding:6px 8px;display:flex;justify-content:space-between;align-items:center}
.kcol-c{font-size:11px;font-weight:700;color:#9ca3af;background:var(--white);border-radius:10px;padding:1px 8px}
.kcard{background:var(--white);border:1px solid var(--border);border-radius:10px;padding:12px;margin-top:8px;cursor:pointer;transition:box-shadow .15s}
.kcard:hover{box-shadow:0 4px 12px rgba(0,0,0,.08)}
.kcard-n{font-size:13px;font-weight:700;margin-bottom:3px}
.kcard-m{font-size:11px;color:var(--muted)}
.kcard-amt{font-size:12px;font-weight:700;color:var(--primary);margin-top:6px}
/* Client card */
.cc-top{display:flex;align-items:center;gap:14px;margin-bottom:18px}
.cc-av{width:48px;height:48px;border-radius:12px;background:#6366F1;display:flex;align-items:center;justify-content:center;font-weight:700;color:#fff;font-size:20px}
.cc-name{font-size:20px;font-weight:800;font-family:'Montserrat'}
.cc-meta{font-size:13px;color:var(--muted);margin-top:2px}
.cc-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:18px}
.cc-field{background:var(--white);border:1.5px solid var(--border);border-radius:11px;padding:12px 14px}
.cc-fl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;margin-bottom:5px}
.cc-fi{width:100%;border:none;font-size:14px;font-weight:600;font-family:'Inter';color:var(--text);outline:none;background:transparent}
.cc-fi::placeholder{color:#cbd5e1;font-weight:400}
.cc-sel{width:100%;border:none;font-size:14px;font-weight:600;font-family:'Inter';outline:none;background:transparent;cursor:pointer}
.cc-actions{display:flex;gap:10px;margin-bottom:18px}
.btn{font-family:'Inter';font-weight:600;border:none;cursor:pointer;border-radius:9px;display:inline-flex;align-items:center;gap:7px;padding:9px 16px;font-size:13px}
.btn-p{background:var(--primary);color:#fff}.btn-p:hover{background:var(--dark)}
.btn-g{background:var(--white);border:1.5px solid var(--border);color:var(--muted)}.btn-g:hover{border-color:var(--primary);color:var(--primary)}
/* Main tabs (functional CRM nav) */
.mtabs{display:flex;gap:4px;background:#F1F5F9;border-radius:12px;padding:4px;margin-bottom:18px;flex-wrap:wrap}
.mtab{flex:1;min-width:96px;padding:10px 14px;font-size:13px;font-weight:700;font-family:'Inter';color:var(--muted);cursor:pointer;border:none;background:transparent;border-radius:9px;display:flex;align-items:center;justify-content:center;gap:6px;white-space:nowrap;transition:all .15s}
.mtab:hover{color:var(--text)}
.mtab.active{background:var(--white);color:var(--primary);box-shadow:0 1px 5px rgba(0,0,0,.07)}
.mtab .badge{font-size:10px;font-weight:800;background:#DC2626;color:#fff;border-radius:10px;padding:1px 7px;line-height:1.5}
/* Tabs (analysis) */
.tabs{display:flex;gap:2px;border-bottom:1.5px solid var(--border);margin-bottom:18px;overflow-x:auto}
.tab{padding:11px 16px;font-size:13px;font-weight:600;color:var(--muted);cursor:pointer;border-bottom:2.5px solid transparent;display:flex;align-items:center;gap:6px}
.tab:hover{color:var(--text)}.tab.active{color:var(--primary);border-bottom-color:var(--primary)}
.tab.done::after{content:'✓';color:var(--mid);font-weight:800}
.run-card{background:var(--white);border:1.5px solid var(--border);border-radius:14px;padding:26px;text-align:center;max-width:520px;margin:8px auto 14px}
.run-ic{font-size:32px;margin-bottom:12px}.run-t{font-family:'Montserrat';font-weight:800;font-size:18px;margin-bottom:8px}
.run-d{font-size:14px;color:var(--muted);line-height:1.5;margin-bottom:18px}
.run-btn{padding:11px 22px;border-radius:10px;background:linear-gradient(135deg,var(--dark),var(--primary));color:#fff;border:none;cursor:pointer;font-weight:700;font-size:14px;font-family:'Inter'}
.cpbar{background:var(--white);border:1.5px solid var(--border);border-radius:12px;padding:13px 16px;display:flex;align-items:center;gap:12px;margin-top:18px}
.cpbar.appr{background:#F0FDF4;border-color:rgba(16,185,129,.3)}
.cpbar-t{flex:1;font-size:13px;font-weight:600}
.cp-btn{padding:8px 16px;border-radius:8px;font-size:12px;font-weight:700;cursor:pointer;border:none;font-family:'Inter'}
.cp-a{background:var(--primary);color:#fff}.cp-r{background:transparent;border:1px solid var(--border);color:var(--muted)}
.blk{background:var(--white);border:1px solid var(--border);border-radius:12px;padding:14px;margin-bottom:10px}
.blk-pain{font-size:12px;color:#92400e;padding-left:14px;position:relative;margin-bottom:3px;line-height:1.4}.blk-pain::before{content:'•';position:absolute;left:3px;color:#f59e0b}
.canvas-grid{display:grid;grid-template-columns:repeat(5,1fr);grid-auto-rows:minmax(80px,auto);gap:8px}
.cv{background:var(--white);border:1px solid var(--border);border-radius:10px;padding:10px}
.cv-h{font-size:10px;font-weight:700;text-transform:uppercase;color:var(--primary);margin-bottom:5px;display:flex;justify-content:space-between}.cv-h span{color:#9ca3af}
.cv li{font-size:11px;color:#374151;line-height:1.3;list-style:none;padding-left:9px;position:relative;margin-bottom:2px}.cv li::before{content:'';position:absolute;left:2px;top:6px;width:3px;height:3px;border-radius:50%;background:var(--mid)}
.cv.vp{grid-column:3;grid-row:span 2;border-color:var(--primary)}.cv.kp{grid-row:span 2}.cv.cs{grid-column:5;grid-row:span 2}
.cv-ins{grid-column:1/-1;background:var(--ink);color:#fff;border-radius:10px;padding:13px 16px;font-size:13px;line-height:1.5}.cv-ins b{color:var(--mid)}
.idef-lbl{font-size:11px;font-weight:700;text-transform:uppercase;color:#9ca3af;margin:14px 0 8px}
.idef{margin-bottom:12px}.idef-c,.idef-m{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;padding:4px 0}
.idef-c{border-bottom:2px dashed #cbd5e1}.idef-m{border-top:2px dashed #cbd5e1}
.idef-mid{display:grid;grid-template-columns:auto 1fr auto;gap:8px;align-items:stretch}
.idef-i,.idef-o{display:flex;flex-direction:column;gap:4px;justify-content:center;max-width:130px}
.idef-fn{background:var(--white);border:2px solid var(--primary);border-radius:10px;padding:10px 9px;text-align:center;font-size:12px;font-weight:700;color:var(--dark);display:flex;flex-direction:column;gap:3px;justify-content:center}
.idef-fn b{font-size:10px;color:var(--primary);font-family:'Montserrat'}.idef-fn i{font-style:normal;font-size:10px;font-weight:700}
.ar{font-size:10px;padding:3px 7px;border-radius:6px;background:#F1F5F9;color:#475569}
.idef-c .ar{background:#FEF3C7;color:#92400E}.idef-c .ar.nomiss{background:#FEF2F2;color:#DC2626;border:1px dashed #FECACA}
.idef-i .ar{background:#EFF6FF;color:#1E40AF}.idef-o .ar{background:#ECFDF5;color:#047857}.idef-o .ar.dead{background:#FEF2F2;color:#DC2626}.idef-m .ar{background:#F5F3FF;color:#6D28D9}
.spec-h{font-family:'Montserrat';font-weight:800;font-size:15px;margin:16px 0 10px;display:flex;align-items:center;gap:8px}.spec-h .pl{width:22px;height:22px;border-radius:6px;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800}
.mod{background:var(--white);border:1px solid var(--border);border-radius:11px;padding:14px;margin-bottom:9px}
.mod-node{font-size:10px;font-weight:700;color:var(--primary);background:var(--light);padding:2px 7px;border-radius:5px}
.scr{font-size:11px;background:#EFF6FF;color:#1E40AF;border:1px solid #BFDBFE;border-radius:6px;padding:3px 8px;display:inline-block;margin:3px 3px 0 0}
.ent{background:var(--white);border:1px solid var(--border);border-radius:10px;padding:12px;margin-bottom:7px}
.ent-fields{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:5px;margin-bottom:7px}
.fld{font-size:11px;background:var(--bg);border-radius:5px;padding:4px 7px}.fld em{font-style:normal;color:#6366F1;font-size:10px}
.ent-ex{font-size:10px;color:#6b7280;font-family:monospace;background:var(--bg);border-radius:5px;padding:6px 8px}
.spin{display:inline-block;animation:sp 1s linear infinite}@keyframes sp{to{transform:rotate(360deg)}}
.empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#cbd5e1;gap:10px;height:100%}
::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-thumb{background:rgba(0,0,0,.12);border-radius:4px}
</style>
</head>
<body>
<header class="hdr"><div class="hdr-ic">@</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 style="width:8px;height:8px;border-radius:50%;background:var(--mid)"></span>Руслан</div></header>
<div class="layout">
<aside class="sb">
<div class="sb-nav">
<div class="nav-item active" id="nav-dash" onclick="setView('dashboard')"><span class="ic">📊</span> Дашборд</div>
<div class="nav-item" id="nav-pipe" onclick="setView('pipeline')"><span class="ic">🎯</span> Воронка</div>
</div>
<button class="sb-new" onclick="newClient()">+ Новый клиент</button>
<div class="sb-cap">Клиенты</div>
<div class="sb-list" id="clientList"></div>
</aside>
<main class="main"><div class="scroll" id="view"></div></main>
</div>
<script>
const API=location.hostname.indexOf("wasrusgen1.ru")>=0?"/consulting":"https://claude-83-172-150-111.sslip.io/elena";
let projects=[], current=null, state=null, view="dashboard", activeTab="interview", mainTab="deal";
const PIPE=[["lead","Лид","#9ca3af"],["qualified","Квалификация","#3B82F6"],["proposal","Предложение","#8B5CF6"],["active","В работе","#047857"],["done","Завершён","#10B981"]];
const pipeMap=Object.fromEntries(PIPE.map(p=>[p[0],p]));
function esc(s){return (s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}
function fmt(s){return esc(s).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>")}
function money(n){return (n||0).toLocaleString("ru-RU")+" ₽"}
// ── UI-состояние: сворачивание секций + фильтры (запоминается) ──
const ui=(()=>{try{return JSON.parse(localStorage.getItem('crm_ui'))||{}}catch(e){return{}}})();
ui.collapsed=ui.collapsed||{revenue:true}; // динамика свёрнута по умолчанию
if(ui.collapsed.revenue===undefined)ui.collapsed.revenue=true;
ui.clientFilter=ui.clientFilter||'all';
ui.taskFilter=ui.taskFilter||'all';
function saveUi(){try{localStorage.setItem('crm_ui',JSON.stringify(ui))}catch(e){}}
function toggleSec(k){ui.collapsed[k]=!ui.collapsed[k];saveUi();renderDashboard();}
function setClientFilter(f){ui.clientFilter=f;saveUi();renderDashboard();}
function setTaskFilter(f){ui.taskFilter=f;saveUi();renderDashboard();}
function secHead(k,title,right){
const col=!!ui.collapsed[k];
return `<div class="sec-h collapsible" onclick="toggleSec('${k}')">
<span class="sec-chev" style="transform:rotate(${col?-90:0}deg)">▼</span>
<span>${title}</span>
${right?`<span style="margin-left:auto;display:flex;align-items:center;flex-wrap:wrap" onclick="event.stopPropagation()">${right}</span>`:''}
</div>`;
}
function chips(cur,opts,fn){return opts.map(([k,n])=>`<button class="fchip ${cur===k?'on':''}" onclick="${fn}('${k}')">${n}</button>`).join('');}
async function loadProjects(){
const r=await fetch(`${API}/api/projects`);const d=await r.json();projects=d.projects;renderClientList();
}
function renderClientList(){
document.getElementById("clientList").innerHTML=projects.map(p=>{
const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];
return `<div class="cl ${p.token===current?'active':''}" onclick="openClient('${p.token}')"><div class="cl-av">${esc((p.client_name||'?')[0])}</div><div class="cl-n">${esc(p.client_name)}</div><div class="cl-dot" style="background:${pc[2]}"></div></div>`;
}).join("")||'<div style="color:rgba(255,255,255,.3);font-size:12px;padding:10px;text-align:center">Пока нет клиентов</div>';
}
async function newClient(){
const name=prompt("Имя клиента / компания:");if(name===null)return;
const niche=prompt("Ниша:")||"";
const r=await fetch(`${API}/api/project/new`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({client_name:name,niche})});
const d=await r.json();
await fetch(`${API}/api/project/profile`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:d.token,client_name:name,niche,description:""})});
await loadProjects();openClient(d.token);
}
function setView(v){view=v;current=null;document.getElementById("nav-dash").classList.toggle("active",v==="dashboard");document.getElementById("nav-pipe").classList.toggle("active",v==="pipeline");renderClientList();render();}
async function openClient(token){
current=token;view="client";mainTab="deal";document.querySelectorAll(".nav-item").forEach(n=>n.classList.remove("active"));
const r=await fetch(`${API}/api/project/${token}`);state=await r.json();renderClientList();render();
}
function render(){
if(view==="dashboard")renderDashboard();
else if(view==="pipeline")renderPipeline();
else if(view==="client")renderClient();
}
function renderDashboard(){
const total=projects.length;
const byPipe=k=>projects.filter(p=>((p.crm&&p.crm.pipeline)||"lead")===k).length;
const active=byPipe("active"),done=byPipe("done"),leads=byPipe("lead");
const paidOf=p=>{const c=p.crm||{};return (c.payments||[]).reduce((s,x)=>s+(x.amount||0),0)||c.paid_amount||0};
const revenue=projects.reduce((s,p)=>s+paidOf(p),0);
const expected=projects.reduce((s,p)=>{const c=p.crm||{};return s+Math.max(0,(c.deal_amount||0)-paidOf(p))},0);
const inwork=projects.filter(p=>((p.crm&&p.crm.pipeline)||"")==="active").reduce((s,p)=>s+((p.crm&&p.crm.deal_amount)||0),0);
const conv=total?Math.round(done/total*100):0;
document.getElementById("view").innerHTML=`
<div class="sec-h">Дашборд</div>
<div class="kpis">
<div class="kpi"><div class="kpi-v">${leads}</div><div class="kpi-l">Новых лидов</div><div class="kpi-sub">${active} в работе</div></div>
<div class="kpi"><div class="kpi-v">${money(revenue)}</div><div class="kpi-l">Выручка (получено)</div></div>
<div class="kpi"><div class="kpi-v">${money(expected)}</div><div class="kpi-l">Ожидается (остатки)</div><div class="kpi-sub">${money(inwork)} в активных сделках</div></div>
<div class="kpi"><div class="kpi-v">${conv}%</div><div class="kpi-l">Конверсия в сделку</div><div class="kpi-sub">${done} завершено</div></div>
</div>
${renderRevenueChart()}
${renderUpcomingTasks()}
${(()=>{
const cf=ui.clientFilter;
const pipeOf=p=>(p.crm&&p.crm.pipeline)||'lead';
let cl=projects;
if(cf==='lead')cl=projects.filter(p=>pipeOf(p)==='lead');
else if(cf==='active')cl=projects.filter(p=>pipeOf(p)==='active');
else if(cf==='done')cl=projects.filter(p=>pipeOf(p)==='done');
else if(cf==='free')cl=projects.filter(p=>((p.crm&&p.crm.billing_type))==='free');
const cChips=chips(cf,[['all','Все'],['lead','Лиды'],['active','В работе'],['done','Завершён'],['free','Бесплатные']],'setClientFilter');
const head=secHead('clients',`Все клиенты · ${cl.length}`,cChips);
if(ui.collapsed.clients)return head;
if(!projects.length)return head+'<div class="empty">Создайте первого клиента</div>';
if(!cl.length)return head+'<div class="tbl"><div class="cl-row" style="color:#9CA3AF;font-size:12px;justify-content:center">Нет клиентов по фильтру</div></div>';
return head+`<div class="tbl">${cl.map(p=>renderClientRow(p)).join("")}</div>`;
})()}`;
}
const STAGE_DEFS=[{key:"interview",name:"Интервью"},{key:"methods",name:"Методологии"},{key:"canvas",name:"Стратегия"},{key:"idef0",name:"Функции"},{key:"spec",name:"ТЗ"}];
function clientStages(p){
const done=[p.msg_count>0,!!p.has_selection,!!p.has_canvas,!!p.has_idef0,!!p.has_spec];
const cnt=done.filter(Boolean).length;
let cur=done.findIndex(d=>!d); // первый незавершённый
const all=cur===-1;
const label=all?"Готово · ТЗ собрано":STAGE_DEFS[cur].name;
return {done,cnt,cur:all?STAGE_DEFS.length:cur,all,label};
}
function stepper(p){
const st=clientStages(p);
let dots="";
STAGE_DEFS.forEach((s,i)=>{
const isDone=st.done[i], isCur=!st.all&&i===st.cur;
const bg=isDone?"#047857":isCur?"#10B981":"#E5E7EB";
const ring=isCur?"box-shadow:0 0 0 3px rgba(16,185,129,.25);":"";
dots+=`<span title="${s.name}${isDone?' ✓':isCur?' — текущий':''}" style="width:11px;height:11px;border-radius:50%;background:${bg};${ring}flex-shrink:0;display:inline-block"></span>`;
if(i<STAGE_DEFS.length-1){const lc=st.done[i]?"#047857":"#E5E7EB";dots+=`<span style="height:2px;flex:1;min-width:14px;background:${lc};display:inline-block"></span>`;}
});
const col=st.all?"#047857":"#10B981";
return `<div style="display:flex;align-items:center;gap:0;margin-top:8px;max-width:360px">${dots}</div>
<div style="font-size:11px;color:var(--muted);margin-top:4px">Этап: <b style="color:${col}">${st.label}</b> · ${st.cnt}/5</div>`;
}
function stepperInline(p,st){
st=st||clientStages(p);
let dots="";
STAGE_DEFS.forEach((s,i)=>{
const isDone=st.done[i], isCur=!st.all&&i===st.cur;
const bg=isDone?"#047857":isCur?"#10B981":"#E5E7EB";
const ring=isCur?"box-shadow:0 0 0 2.5px rgba(16,185,129,.25);":"";
dots+=`<span title="${s.name}${isDone?' ✓':isCur?' — текущий':''}" style="width:9px;height:9px;border-radius:50%;background:${bg};${ring}flex-shrink:0;display:inline-block"></span>`;
if(i<STAGE_DEFS.length-1){const lc=st.done[i]?"#047857":"#E5E7EB";dots+=`<span style="height:2px;width:13px;background:${lc};display:inline-block"></span>`;}
});
const col=st.all?"#047857":"#10B981";
return `<div style="display:flex;align-items:center;gap:9px;margin-top:5px"><span style="display:flex;align-items:center">${dots}</span><span style="font-size:11px;color:var(--muted);white-space:nowrap">Этап: <b style="color:${col}">${st.label}</b> · ${st.cnt}/5</span></div>`;
}
function renderClientRow(p){
const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];
const billing=(p.crm&&p.crm.billing_type)||"paid";
const bChip=billing==="free"
?`<span class="mini-chip" style="color:#6366F1;background:#EEF2FF">🎁 Беспл.</span>`
:`<span class="mini-chip" style="color:#047857;background:#ECFDF5">💰 Платный</span>`;
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 style="flex:1;min-width:0">
<div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap"><span style="font-weight:700;font-size:13.5px">${esc(p.client_name)}</span>${bChip}<span style="font-size:11.5px;color:var(--muted)">${esc(p.niche)} · ${p.msg_count} сообщ.</span></div>
${stepperInline(p)}
</div>
<span class="mini-chip" style="color:${pc[2]};background:${pc[2]}1a">${pc[1]}</span>
<span style="font-weight:800;color:var(--primary);min-width:80px;text-align:right;font-size:14px">${money((p.crm&&p.crm.deal_amount)||0)}</span>
</div>`;
}
function renderRevenueChart(){
const months={};
projects.forEach(p=>(p.crm&&p.crm.payments||[]).forEach(pay=>{const m=(pay.date||"").slice(0,7);if(m)months[m]=(months[m]||0)+(pay.amount||0)}));
const keys=Object.keys(months).sort();
if(!keys.length)return"";
const max=Math.max(...keys.map(k=>months[k]));
const MN=["янв","фев","мар","апр","май","июн","июл","авг","сен","окт","ноя","дек"];
const lbl=k=>{const[y,m]=k.split("-");return MN[+m-1]+" "+y.slice(2)};
const totalRev=keys.reduce((s,k)=>s+months[k],0);
const head=secHead('revenue',`📈 Выручка по месяцам · ${money(totalRev)}`);
if(ui.collapsed.revenue)return head;
return head+`<div class="blk" style="padding:14px 16px;margin-bottom:14px"><div style="display:flex;align-items:flex-end;justify-content:flex-start;gap:18px;height:104px">${keys.map(k=>`<div style="flex:0 0 56px;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%"><div style="font-size:11px;font-weight:700;color:#047857;margin-bottom:4px">${(months[k]/1000).toFixed(0)}к</div><div style="width:100%;background:linear-gradient(180deg,#10B981,#047857);border-radius:6px 6px 0 0;height:${Math.max(8,months[k]/max*100)}%"></div><div style="font-size:11px;color:var(--muted);margin-top:6px">${lbl(k)}</div></div>`).join("")}</div></div>`;
}
function renderUpcomingTasks(){
const today=new Date().toISOString().slice(0,10);
const all=[];
projects.forEach(p=>(p.tasks||[]).forEach(t=>{if(!t.done)all.push({...t,client:p.client_name,token:p.token})}));
all.sort((a,b)=>(a.due||"9999")<(b.due||"9999")?-1:1);
if(!all.length)return"";
const f=ui.taskFilter;
let list=all;
if(f==='overdue')list=all.filter(t=>t.due&&t.due<today);
else if(f==='today')list=all.filter(t=>t.due===today);
const overdueN=all.filter(t=>t.due&&t.due<today).length;
const filterChips=chips(f,[['all','Все'],['today','Сегодня'],['overdue',`Просроченные${overdueN?' '+overdueN:''}`]],'setTaskFilter');
const head=secHead('tasks',`📌 Задачи · ${all.length}`,filterChips);
if(ui.collapsed.tasks)return head;
const top=list.slice(0,10);
if(!top.length)return head+'<div class="tbl"><div class="tbl-row" style="color:#9CA3AF;font-size:12px;justify-content:center">Нет задач по фильтру</div></div>';
return head+`<div class="tbl">${top.map(t=>`<div class="tbl-row" onclick="openClient('${t.token}')"><span style="font-size:13px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(t.text)}</span><span style="font-size:12px;color:var(--muted);white-space:nowrap">${esc(t.client)}</span>${t.due?`<span style="font-size:11px;font-weight:700;padding:3px 9px;border-radius:6px;white-space:nowrap;${t.due<today?'background:#FEF2F2;color:#DC2626':t.due===today?'background:#FEF3C7;color:#92400E':'background:#F1F5F9;color:#6B7280'}">${t.due===today?'сегодня':t.due<today?'просрочено':fmtDate(t.due)}</span>`:''}</div>`).join("")}</div>`;
}
function renderPipeline(){
document.getElementById("view").innerHTML=`<div class="sec-h">Воронка продаж</div><div class="kanban">${PIPE.map(([k,name,col])=>{
const items=projects.filter(p=>((p.crm&&p.crm.pipeline)||"lead")===k);
return `<div class="kcol"><div class="kcol-h"><span style="color:${col}">${name}</span><span class="kcol-c">${items.length}</span></div>${items.map(p=>`<div class="kcard" onclick="openClient('${p.token}')"><div class="kcard-n">${esc(p.client_name)}</div><div class="kcard-m">${esc(p.niche)}</div>${(p.crm&&p.crm.deal_amount)?`<div class="kcard-amt">${money(p.crm.deal_amount)}</div>`:''}</div>`).join("")}</div>`;
}).join("")}</div>`;
}
const MAINTABS=[{id:"deal",name:"Сделка",icon:"📇"},{id:"pricing",name:"Ценообразование",icon:"💰"},{id:"payments",name:"Платежи",icon:"💳"},{id:"tasks",name:"Задачи",icon:"📌"},{id:"analysis",name:"Анализ",icon:"📊"}];
function renderClient(){
const crm=state.crm||{pipeline:"lead",deal_amount:0,paid_amount:0,contact:"",source:"",note:""};
const billing=crm.billing_type||"paid";
const deal=crm.deal_amount||0, pays=crm.payments||[], paid=pays.reduce((s,p)=>s+(p.amount||0),0), left=deal-paid;
const today=new Date().toISOString().slice(0,10);
const overdue=(state.tasks||[]).filter(t=>!t.done&&t.due&&t.due<today).length;
const badge=id=>{if(id==="payments"&&deal>0&&left>0)return `<span class="badge">${money(left)}</span>`;if(id==="tasks"&&overdue)return `<span class="badge">${overdue}</span>`;return ''};
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 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('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>
</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 id="mainPanel"></div>`;
renderMainPanel();
}
function setMainTab(t){mainTab=t;renderMainPanel();document.querySelectorAll('.mtab').forEach((b,i)=>b.classList.toggle('active',MAINTABS[i].id===mainTab));}
function renderMainPanel(){
const p=document.getElementById("mainPanel");if(!p)return;
const crm=state.crm||{pipeline:"lead",deal_amount:0,source:""};
if(mainTab==="deal"){
const deal=crm.deal_amount||0;
const pays=crm.payments||[]; const paid=pays.reduce((s,x)=>s+(x.amount||0),0); const left=Math.max(0,deal-paid);
const doneArr=CLIENT_STAGES.map(s=>stageIsDone(s.key));
const firstUndone=doneArr.findIndex(d=>!d);
const doneCnt=doneArr.filter(Boolean).length;
p.innerHTML=`
<div class="cc-grid">
<div class="cc-field"><div class="cc-fl">Статус воронки</div><select class="cc-sel" id="cpPipe" onchange="saveCrm()">${PIPE.map(([k,n])=>`<option value="${k}" ${crm.pipeline===k?'selected':''}>${n}</option>`).join("")}</select></div>
<div class="cc-field"><div class="cc-fl">Сумма сделки</div><input class="cc-fi" id="cpDeal" type="number" value="${crm.deal_amount||''}" placeholder="0" 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>
<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 style="display:grid;grid-template-columns:1.35fr 1fr;gap:14px;align-items:start">
<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>
${CLIENT_STAGES.map((s,i)=>{
const done=doneArr[i], cur=!done&&i===firstUndone;
const brd=done?'#047857':cur?'#10B981':'#E5E7EB';
const tag=done?'<span style="font-size:10px;font-weight:700;color:#047857;background:#ECFDF5;padding:1px 7px;border-radius:5px">✓ готово</span>':cur?'<span style="font-size:10px;font-weight:700;color:#92400E;background:#FEF3C7;padding:1px 7px;border-radius:5px">● в работе</span>':'<span style="font-size:10px;font-weight:700;color:#9CA3AF;background:#F3F4F6;padding:1px 7px;border-radius:5px">ожидает</span>';
return `<div style="display:flex;align-items:center;gap:10px;padding:7px 0;border-top:1px solid var(--bg)">
<span style="width:24px;height:24px;border-radius:6px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:12px;background:${done||cur?'#ECFDF5':'#F9FAFB'};border:1.5px solid ${brd}">${s.icon}</span>
<div style="flex:1;min-width:0"><div style="font-size:12.5px;font-weight:700;color:${done||cur?'var(--text)':'#9CA3AF'}">${s.name}</div><div style="font-size:11px;color:var(--muted)">${esc(s.desc)}</div></div>
${tag}
</div>`;
}).join('')}
</div>
<div style="display:flex;flex-direction:column;gap:14px">
<div class="blk" style="margin:0">
<div style="font-size:13px;font-weight:700;margin-bottom:8px">💰 Финансы</div>
<div style="display:flex;justify-content:space-between;padding:5px 0;font-size:13px"><span style="color:var(--muted)">Смета</span><b>${deal>0?money(deal):'—'}</b></div>
<div style="display:flex;justify-content:space-between;padding:5px 0;font-size:13px;border-top:1px solid var(--bg)"><span style="color:var(--muted)">Получено</span><b style="color:#047857">${money(paid)}</b></div>
<div style="display:flex;justify-content:space-between;padding:5px 0;font-size:13px;border-top:1px solid var(--bg)"><span style="color:var(--muted)">Остаток</span><b style="color:${left>0?'#DC2626':'#047857'}">${money(left)}</b></div>
</div>
<div class="blk" style="margin:0">
<div class="cc-fl">Контакт</div>
<input class="cc-fi" id="cpContact" value="${esc(crm.contact||'')}" placeholder="телефон / email" onchange="saveCrm()" style="margin-bottom:12px">
<div class="cc-fl">Заметка</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>
</div>
</div>
</div>`;
renderPayments();
}
else if(mainTab==="pricing"){p.innerHTML=`<div id="pricingBox"></div>`;renderPricing();}
else if(mainTab==="payments"){p.innerHTML=`<div id="planBox"></div><div id="paymentsBox"></div>`;renderPaymentPlan();renderPayments();}
else if(mainTab==="tasks"){p.innerHTML=`<div id="tasksBox"></div>`;renderTasks();}
else if(mainTab==="analysis"){p.innerHTML=`<div class="tabs">${TABS.map(t=>`<div class="tab ${t.id===activeTab?'active':''} ${approved(t.id)?'done':''}" onclick="setTab('${t.id}')">${t.icon} ${t.name}</div>`).join("")}</div><div id="tabContent"></div>`;renderTab();}
}
function inviteTelegram(){const url=`https://t.me/wasrusgen1_consulting_bot?start=${current}`;navigator.clipboard.writeText(url).then(()=>alert("Ссылка для Telegram скопирована:\n\n"+url+"\n\nКлиент откроет кабинет прямо в Telegram.")).catch(()=>prompt("Ссылка для Telegram:",url));}
async function setBilling(t){
state.crm=state.crm||{};state.crm.billing_type=t;
await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,billing_type:t})});
renderClient();await loadProjects();
}
// ── План оплаты (привязка к вехам анализа) ──
// ── Этапы оплаты — от прогресса клиента ──────────────────
// Этапы = 5 этапов анализа. Отмечаем оплату по мере прохождения.
// Правило: ТЗ (печать + выгрузка) только после 100% оплаты.
const CLIENT_STAGES=[
{key:"interview", name:"Интервью", icon:"💬", desc:"Диагностика «как есть» — карта проблем"},
{key:"methods", name:"Методологии", icon:"🎯", desc:"Подбор фреймворков под вашу задачу"},
{key:"canvas", name:"Стратегия", icon:"📊", desc:"Целевая модель процессов (TO-BE)"},
{key:"idef0", name:"Функции IDEF0", icon:"🔧", desc:"Декомпозиция функций + регламенты"},
{key:"spec", name:"Выдача ТЗ", icon:"📋", desc:"Готовое ТЗ к внедрению"},
];
// ── Смета: базовые ставки модулей (интервью = вход 0₽) ──
const STAGE_BASE={interview:0, methods:8000, canvas:12000, idef0:15000, spec:5000};
const CX_COEF={low:1.0, medium:1.5, high:2.0};
const CX_LABEL={low:"Простой · 13 процесса", medium:"Средний · 47", high:"Сложный · 8+"};
const CX_SHORT={low:"Простой ×1.0", medium:"Средний ×1.5", high:"Сложный ×2.0"};
function getStagePrices(){return (state.crm&&state.crm.stage_prices)||null;}
function getComplexity(){return (state.crm&&state.crm.complexity)||"medium";}
function recalcDeal(sp){return Object.values(sp).reduce((a,b)=>a+(+b||0),0);}
function buildEstimate(){
const coef=CX_COEF[getComplexity()];
const sp={};
CLIENT_STAGES.forEach(s=>{sp[s.key]=Math.round((STAGE_BASE[s.key]||0)*coef/100)*100;});
state.crm=state.crm||{};
state.crm.stage_prices=sp;
state.crm.complexity=getComplexity();
state.crm.deal_amount=recalcDeal(sp);
saveEstimate();renderClient();
}
async function setComplexity(cx){
state.crm=state.crm||{};state.crm.complexity=cx;
if(state.crm.stage_prices){buildEstimate();return;}
await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,complexity:cx})});
renderClient();
}
function editStagePrice(k){
const cur=(getStagePrices()||{})[k]||0;
const v=prompt(`Цена этапа «${CLIENT_STAGES.find(s=>s.key===k).name}» (₽):`,cur);
if(v===null)return;
const n=Math.max(0,Math.round(+v||0));
state.crm=state.crm||{};
state.crm.stage_prices=state.crm.stage_prices||{};
state.crm.stage_prices[k]=n;
state.crm.deal_amount=recalcDeal(state.crm.stage_prices);
saveEstimate();renderClient();
}
async function saveEstimate(){
await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage_prices:state.crm.stage_prices,complexity:state.crm.complexity,deal_amount:state.crm.deal_amount})});
await loadProjects();
}
function stagePayKey(k){return "pay_"+k;}
function stageIsDone(k){
if(k==="interview") return (state.messages||[]).length>0;
if(k==="methods") return !!state.selection;
if(k==="canvas") return !!state.canvas;
if(k==="idef0") return !!(state.model);
if(k==="spec") return !!state.spec;
return false;
}
function getStagePays(){return (state.crm&&state.crm.stage_payments)||{};}
function markStagePayInput(k){
const fid='spf-'+k;
const ex=document.getElementById(fid);if(ex){ex.remove();return;}
const deal=(state.crm&&state.crm.deal_amount)||0;
const sp=getStagePrices()||{};
const suggested=sp[k]||(deal>0?Math.round(deal/5):'');
const today=new Date().toISOString().slice(0,10);
const wrap=document.createElement('div');
wrap.id=fid;
wrap.style.cssText='display:flex;gap:8px;margin-top:8px;flex-wrap:wrap;align-items:center;padding:10px 12px;background:var(--bg);border-radius:10px;border:1.5px solid #D1FAE5';
wrap.innerHTML=`<input type="number" id="spa-${k}" placeholder="Сумма ₽" value="${suggested}" style="width:110px;border:1.5px solid var(--border);border-radius:8px;padding:7px 10px;font-size:13px;font-family:Inter"><input type="date" id="spd-${k}" value="${today}" style="border:1.5px solid var(--border);border-radius:8px;padding:7px 10px;font-size:13px;font-family:Inter"><input id="spn-${k}" placeholder="Назначение" style="flex:1;min-width:120px;border:1.5px solid var(--border);border-radius:8px;padding:7px 10px;font-size:13px;font-family:Inter"><button class="cp-btn cp-a" style="padding:7px 14px" onclick="confirmStagePay('${k}')">Сохранить</button><button class="cp-btn cp-r" style="padding:7px 12px" onclick="document.getElementById('${fid}').remove()">✕</button>`;
const row=document.getElementById('stagerow-'+k);
if(row)row.parentNode.insertBefore(wrap,row.nextSibling);
}
async function confirmStagePay(k){
const amt=+document.getElementById('spa-'+k).value;
const date=document.getElementById('spd-'+k).value||new Date().toISOString().slice(0,10);
const note=document.getElementById('spn-'+k).value;
if(!amt){alert('Укажите сумму');return;}
const pays=getStagePays();if(pays[k])return;
const stageName=CLIENT_STAGES.find(s=>s.key===k).name;
pays[k]={amount:amt,date};
state.crm=state.crm||{};
state.crm.stage_payments=pays;
state.crm.payments=state.crm.payments||[];
state.crm.payments.push({date,amount:amt,note:note||'Этап: '+stageName,stage:k});
await Promise.all([
fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage_payments:pays,payments:state.crm.payments})}),
loadProjects()
]);
renderClient();
}
function renderPaymentPlan(){
const box=document.getElementById("planBox");if(!box)return;
const billing=(state.crm&&state.crm.billing_type)||"paid";
const sp=getStagePrices();
const pays=getStagePays();
const paidTotal=Object.values(pays).reduce((s,p)=>s+(p.amount||0),0);
// ── Сметы ещё нет → генератор ──
if(!sp){
const cx=getComplexity();
box.innerHTML=`<div class="blk" style="margin-bottom:14px">
<div style="font-size:13px;font-weight:700;margin-bottom:6px">📋 Смета проекта</div>
<div style="font-size:12px;color:var(--muted);line-height:1.5;margin-bottom:14px">После диагностики сформируйте смету. Клиент увидит, <b>за что и сколько</b> платит. Интервью — бесплатный вход, дальше оплата помодульно. ТЗ выдаётся после полной оплаты.</div>
<div style="font-size:12px;font-weight:600;margin-bottom:8px">Сложность проекта (из интервью):</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
${Object.keys(CX_COEF).map(k=>`<button class="cp-btn ${cx===k?'cp-a':'cp-r'}" style="padding:8px 12px" onclick="setComplexity('${k}')">${CX_LABEL[k]}</button>`).join('')}
</div>
<div style="font-size:11px;color:var(--muted);margin-bottom:14px">Предварительно: ${money(recalcDeal(Object.fromEntries(CLIENT_STAGES.map(s=>[s.key,Math.round((STAGE_BASE[s.key]||0)*CX_COEF[cx]/100)*100]))))} за проект</div>
<button class="cp-btn cp-a" style="padding:9px 18px" onclick="buildEstimate()">Сформировать смету →</button>
</div>`;
return;
}
const total=recalcDeal(sp);
const isUnlocked=billing==="free"||(total>0&&paidTotal>=total);
const cx=getComplexity();
// первый ещё не оплаченный платный выполненный модуль — «доступен к оплате»
box.innerHTML=`<div class="blk" style="margin-bottom:14px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;flex-wrap:wrap">
<span style="font-size:13px;font-weight:700">📋 Смета проекта</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>
</div>
<div style="font-size:11px;color:var(--muted);margin-bottom:12px">Получено <b style="color:var(--primary)">${money(paidTotal)}</b> из ${money(total)} · нажмите цену чтобы изменить</div>
${CLIENT_STAGES.map(s=>{
const done=stageIsDone(s.key);
const paid=pays[s.key];
const price=sp[s.key]||0;
const isFree=price<=0;
let badge='', sub='', action='';
if(isFree){
badge=`<span style="font-size:10px;font-weight:700;color:#047857;background:#D1FAE5;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>`;
action=`<span style="font-size:11px;font-weight:700;color:#9CA3AF;white-space:nowrap">0 ₽</span>`;
} else if(paid){
badge=`<span style="font-size:10px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 6px;border-radius:4px;margin-left:4px"> Оплачен</span>`;
sub=`<div style="font-size:11px;color:#047857;margin-top:2px">Оплачено ${money(paid.amount)} · ${fmtDate(paid.date)}</div>`;
action=`<span style="font-size:11px;font-weight:700;color:#047857;background:#ECFDF5;padding:5px 11px;border-radius:8px;white-space:nowrap">${money(price)}</span>`;
} 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>`;
sub=`<div style="font-size:11px;color:var(--muted);margin-top:2px">${esc(s.desc)}</div>`;
action=`<button class="cp-btn cp-a" style="white-space:nowrap" onclick="markStagePayInput('${s.key}')">Оплата · ${money(price)}</button>`;
} else {
badge=`<span style="font-size:10px;font-weight:700;color:#9CA3AF;background:#F3F4F6;padding:1px 6px;border-radius:4px;margin-left:4px">🔒 Ожидает</span>`;
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>`;
}
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>
<div style="flex:1;min-width:0">
<div style="font-weight:700;font-size:13px;color:${done||isFree?'var(--text)':'#9CA3AF'}">${s.name}${badge}</div>
${sub}
</div>
${action}
</div>`;
}).join("")}
<div style="display:flex;align-items:center;gap:10px;padding:12px 0 2px;border-top:2px solid var(--bg);margin-top:4px">
<span style="font-size:13px;font-weight:800;font-family:Montserrat">Итого по проекту</span>
<span style="margin-left:auto;font-size:18px;font-weight:800;color:var(--primary)">${money(total)}</span>
</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'}">
${isUnlocked
?`✅ <b>Оплата получена.</b> Клиент может скачать ТЗ и печатные документы.`
:`🔒 <b>Правило:</b> ТЗ (печать + выгрузка) выдаётся клиенту только после полной оплаты — ${money(total)}.`}
</div>
</div>`;
}
function renderPricing(){
const box=document.getElementById("pricingBox");if(!box)return;
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 p=state.pricing;
if(!p){box.innerHTML=freeNote+`<div class="run-card" style="margin:0 0 18px"><div class="run-ic">💰</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);
}
function pricingCardHTML(p){
const SZ={micro:"Микро",small:"Малый",medium:"Средний",large:"Крупный"};const CX={low:"низкая",medium:"средняя",high:"высокая"};
return `<div class="blk" style="margin-bottom:14px"><div style="display:flex;align-items:center;margin-bottom:10px"><b style="font-size:14px">💰 Ценовое предложение</b><button class="cp-btn cp-r" style="margin-left:auto;padding:5px 10px" onclick="buildPricing()">↻ Пересчитать</button></div>
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap"><span style="font-size:12px;background:#F1F5F9;border-radius:6px;padding:4px 10px">Масштаб: <b>${SZ[p.scale.size]||p.scale.size}</b></span><span style="font-size:12px;background:#F1F5F9;border-radius:6px;padding:4px 10px">Сложность: <b>${CX[p.scale.complexity]||p.scale.complexity}</b></span><span style="font-size:12px;background:#F1F5F9;border-radius:6px;padding:4px 10px">${esc(p.scale.effort_estimate)}</span></div>
<div style="font-size:12px;color:var(--muted);margin-bottom:14px;padding:10px;background:var(--bg);border-radius:8px"><b>Рынок:</b> ${esc(p.market)}</div>
<div style="display:grid;grid-template-columns:repeat(${p.packages.length},1fr);gap:10px">${p.packages.map((pk,i)=>`<div style="border:1.5px solid ${p.recommended&&p.recommended.indexOf(pk.name)>=0?'var(--primary)':'var(--border)'};border-radius:12px;padding:14px;position:relative">${p.recommended&&p.recommended.indexOf(pk.name)>=0?'<span style="position:absolute;top:-9px;left:14px;background:var(--primary);color:#fff;font-size:9px;font-weight:800;padding:2px 8px;border-radius:10px">РЕКОМЕНДУЮ</span>':''}<div style="font-size:14px;font-weight:800;font-family:Montserrat;margin-bottom:4px">${esc(pk.name)}</div><div style="font-size:22px;font-weight:800;color:var(--primary);margin-bottom:2px">${money(pk.price)}</div><div style="font-size:11px;color:#9ca3af;margin-bottom:8px">${esc(pk.duration)}</div><div style="margin-bottom:8px">${pk.scope.map(s=>`<div style="font-size:11.5px;color:#374151;padding-left:14px;position:relative;margin-bottom:3px;line-height:1.35"><span style="position:absolute;left:2px;color:var(--mid)">✓</span>${esc(s)}</div>`).join("")}</div><div style="font-size:11px;color:#6b7280;font-style:italic;border-top:1px solid var(--bg);padding-top:6px">${esc(pk.argument)}</div><button class="cp-btn cp-a" style="width:100%;margin-top:8px" onclick="applyPrice(${pk.price})">Выбрать → ${money(pk.price)}</button></div>`).join("")}</div>
<div style="margin-top:14px;padding:12px 14px;background:linear-gradient(135deg,rgba(4,120,87,.06),rgba(16,185,129,.04));border:1px solid rgba(4,120,87,.15);border-radius:10px;font-size:13px;line-height:1.5"><b style="color:var(--primary)">Аргумент для клиента:</b> ${esc(p.rationale)}</div>
</div>`;
}
async function buildPricing(){
const btn=document.getElementById("rb-pricing");if(btn){btn.disabled=true;btn.innerHTML='<span class="spin">⏳</span> Елена анализирует рынок...'}
try{const r=await fetch(`${API}/api/build-pricing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current})});const d=await r.json();
if(d.error){alert("Ошибка: "+d.error);if(btn){btn.disabled=false;btn.textContent="Повторить"}return}
state.pricing=d.pricing;renderPricing();
}catch(e){alert("Ошибка: "+e.message);if(btn)btn.disabled=false}
}
function applyPrice(price){
state.crm=state.crm||{};state.crm.deal_amount=price;
const el=document.getElementById("cpDeal");if(el)el.value=price;
fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,deal_amount:price})}).then(()=>{loadProjects();alert("Сумма сделки установлена: "+money(price)+"\nПлатежи — во вкладке «Платежи».")});
}
function renderPayments(){
const crm=state.crm||{};const deal=crm.deal_amount||0;const pays=crm.payments||[];
const paid=pays.reduce((s,p)=>s+(p.amount||0),0);const left=deal-paid;
const st=paid<=0?["Не оплачено","#DC2626","#FEF2F2"]:left>0?["Частично","#92400E","#FEF3C7"]:["Оплачено","#047857","#ECFDF5"];
const sb=document.getElementById("payStatusBox");
if(sb)sb.innerHTML=`<div class="cc-fl">Оплата</div><div style="display:flex;align-items:center;gap:8px"><span style="font-size:14px;font-weight:700;color:${st[1]}">${money(paid)}</span>${deal>0?`<span style="font-size:11px;font-weight:700;color:${st[1]};background:${st[2]};padding:2px 8px;border-radius:6px">${st[0]}</span>`:''}</div>`;
const box=document.getElementById("paymentsBox");if(!box)return;
box.innerHTML=`<div style="background:var(--white);border:1.5px solid var(--border);border-radius:12px;padding:14px 16px;margin-bottom:18px">
<div style="display:flex;align-items:center;margin-bottom:10px;flex-wrap:wrap;gap:8px"><span style="font-size:13px;font-weight:700">💰 Платежи</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>`:''}
${pays.length?`<button class="cp-btn cp-r" style="padding:5px 10px" onclick="exportPayments()">⬇ CSV</button>`:''}</div>
${pays.map((p,i)=>`<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 style="flex:1;font-size:13px">${esc(p.note||'Платёж')}</span><span style="font-size:13px;font-weight:700;color:#047857">${money(p.amount)}</span><button onclick="delPayment(${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;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="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()">
<select id="payStage" style="border:1.5px solid var(--border);border-radius:8px;padding:8px 10px;font-size:13px;font-family:Inter;background:var(--white);color:var(--text)">
<option value="">— без этапа —</option>
${CLIENT_STAGES.filter(s=>stageIsDone(s.key)&&!getStagePays()[s.key]&&((getStagePrices()||{})[s.key]||0)>0).map(s=>`<option value="${s.key}">${s.name} · ${money((getStagePrices()||{})[s.key])}</option>`).join('')}
</select>
<input id="payNote" placeholder="Назначение" style="flex:1;min-width:120px;border:1.5px solid var(--border);border-radius:8px;padding:8px 11px;font-size:13px;font-family:Inter" onkeydown="if(event.key==='Enter')addPayment()">
<button class="cp-btn cp-a" onclick="addPayment()">+ Платёж</button>
</div>
</div>`;
}
async function savePayments(){
await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,payments:state.crm.payments,stage_payments:state.crm.stage_payments||{}})});
await loadProjects();
}
function addPayment(){
const amt=+document.getElementById("payAmt").value;if(!amt){alert("Укажите сумму");return;}
const date=document.getElementById("payDate").value;
const note=document.getElementById("payNote").value;
const stageKey=document.getElementById("payStage")?.value||"";
const stageName=stageKey?CLIENT_STAGES.find(s=>s.key===stageKey)?.name:"";
state.crm=state.crm||{};
state.crm.payments=state.crm.payments||[];
state.crm.payments.push({date,amount:amt,note:note||(stageName?"Этап: "+stageName:"Платёж"),stage:stageKey||null});
if(stageKey&&!getStagePays()[stageKey]){
state.crm.stage_payments=state.crm.stage_payments||{};
state.crm.stage_payments[stageKey]={amount:amt,date};
}
savePayments();renderClient();
}
function delPayment(i){state.crm.payments.splice(i,1);savePayments();renderClient();}
function remindBalance(left){
const due=new Date(Date.now()+7*864e5).toISOString().slice(0,10);
state.tasks=state.tasks||[];state.tasks.push({text:`Получить остаток ${money(left)} от ${state.client_name||'клиента'}`,due,done:false});
saveTasks();renderClient();
alert(`Задача создана: «Получить остаток ${money(left)}»\nСрок: через 7 дней`);
}
function exportPayments(){
const pays=state.crm.payments||[];
let csv="Дата;Сумма;Назначение\n"+pays.map(p=>`${p.date||''};${p.amount||0};${(p.note||'').replace(/;/g,',')}`).join("\n");
csv=""+csv; // BOM для Excel
const blob=new Blob([csv],{type:"text/csv;charset=utf-8"});
const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=`Платежи_${(state.client_name||'клиент').replace(/[^а-яёa-z0-9]/gi,'_')}.csv`;a.click();
}
function renderTasks(){
const box=document.getElementById("tasksBox");if(!box)return;
const tasks=state.tasks||[];
const today=new Date().toISOString().slice(0,10);
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">📌 Задачи по клиенту</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"><input id="newTask" placeholder="Новая задача..." style="flex:1;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()"><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>`;
}
function fmtDate(d){const[y,m,dd]=d.split("-");return `${dd}.${m}`}
async function saveTasks(){await fetch(`${API}/api/project/tasks`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,tasks:state.tasks})});await loadProjects();}
function addTask(){const t=document.getElementById("newTask").value.trim();if(!t)return;const due=document.getElementById("newTaskDue").value;state.tasks=state.tasks||[];state.tasks.push({text:t,due,done:false});saveTasks();renderClient();}
function toggleTask(i){state.tasks[i].done=!state.tasks[i].done;saveTasks();renderClient();}
function delTask(i){state.tasks.splice(i,1);saveTasks();renderClient();}
async function saveCrm(){
const g=id=>document.getElementById(id);
const crm={pipeline:g("cpPipe").value,deal_amount:+g("cpDeal").value||0,source:g("cpSrc").value};
if(g("cpContact"))crm.contact=g("cpContact").value;
if(g("cpNote"))crm.note=g("cpNote").value;
state.crm={...state.crm,...crm};
await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,...crm})});
await loadProjects();
}
function inviteLink(){const url=`${location.origin}${location.pathname.replace('crm.html','cabinet.html')}?t=${current}`;navigator.clipboard.writeText(url).then(()=>alert("Ссылка скопирована:\n\n"+url)).catch(()=>prompt("Ссылка:",url));}
async function deleteClient(){
const nm=state.client_name||"клиента";
if(!confirm(`Удалить «${nm}» со всеми данными (интервью, анализ, ТЗ, документы)?\n\nЭто действие необратимо.`))return;
await fetch(`${API}/api/project/delete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current})});
current=null;await loadProjects();setView("dashboard");
}
function exportSpecPDF(){
const s=state.spec;if(!s){alert("ТЗ ещё не собрано");return}
const cn=esc(state.client_name||"Клиент"),nm=esc(state.niche||"");
const w=window.open("","_blank");
let body=`<h2>1. Обзор системы</h2><p>${esc(s.overview)}</p>`;
body+=`<h2>2. Роли пользователей</h2>`;s.roles.forEach(r=>body+=`<div class="item"><b>${esc(r.name)}</b> — ${esc(r.does)}<div class="muted">Доступ: ${esc(r.access)}</div></div>`);
body+=`<h2>3. Модули системы</h2>`;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+=`<h2>4. Модель данных</h2>`;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(s.open_questions&&s.open_questions.length){body+=`<h2>5. Уточнить перед разработкой</h2><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>
@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}
.cover{border-bottom:3px solid #047857;padding-bottom:16px;margin-bottom:24px}
.brand{font-size:12px;font-weight:700;color:#047857;letter-spacing:.05em}.title{font-size:26px;font-weight:800;margin:8px 0}.sub{color:#6B7280}
h2{font-size:17px;color:#064E3B;border-left:4px solid #047857;padding-left:10px;margin:24px 0 12px}
.item{border:1px solid #E5E7EB;border-radius:8px;padding:12px;margin-bottom:10px;page-break-inside:avoid}
.muted{color:#6B7280;font-size:13px;margin-top:3px}.tag{background:#ECFDF5;color:#047857;font-size:11px;font-weight:700;padding:2px 7px;border-radius:5px}
table{width:100%;border-collapse:collapse;margin:8px 0;font-size:13px}th,td{border:1px solid #E5E7EB;padding:5px 9px;text-align:left}th{background:#F5F6F8}
.ex{font-family:monospace;font-size:11px;background:#F5F6F8;padding:6px 9px;border-radius:5px;margin-top:6px;color:#374151}
ul{margin:4px 0 4px 20px}li{margin-bottom:3px}
@media print{.noprint{display:none}}
</style></head><body>
<div class="cover"><div class="brand">@wasrusgen1 · КОНСАЛТИНГ</div><div class="title">Техническое задание</div><div class="sub">${cn}${nm?" · "+nm:""} · ${new Date().toLocaleDateString("ru-RU")}</div></div>
${body}
<button class="noprint" onclick="window.print()" style="position:fixed;bottom:20px;right:20px;padding:12px 22px;background:#047857;color:#fff;border:none;border-radius:9px;font-size:14px;font-weight:700;cursor:pointer">⬇ Сохранить в PDF</button>
</body></html>`);
w.document.close();
}
async function designScreen(module,idx){
const box=document.getElementById(`screen-${idx}`);box.innerHTML='<div style="font-size:12px;color:#9ca3af"><span class="spin">⏳</span> Елена рисует экран...</div>';
try{const r=await fetch(`${API}/api/design-screen`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,module})});const d=await r.json();
if(d.error){box.innerHTML=`<div style="color:#DC2626;font-size:12px">Ошибка: ${d.error}</div>`;return}
box.innerHTML=renderScreen(d.screen);
}catch(e){box.innerHTML=`<div style="color:#DC2626">${e.message}</div>`}
}
function renderScreen(s){
const frame=inner=>`<div style="border:1.5px solid var(--border);border-radius:12px;overflow:hidden;background:#fff;width:100%"><div style="background:#0F0F1A;padding:8px 14px;display:flex;align-items:center;gap:8px"><span style="display:flex;gap:5px"><span style="width:9px;height:9px;border-radius:50%;background:#FF5F57"></span><span style="width:9px;height:9px;border-radius:50%;background:#FEBC2E"></span><span style="width:9px;height:9px;border-radius:50%;background:#28C840"></span></span><span style="color:rgba(255,255,255,.6);font-size:11px;margin-left:6px">${esc(s.title)}</span></div><div style="padding:16px">${inner}</div></div>`;
const acts=`<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px">${(s.actions||[]).map((a,i)=>`<span style="font-size:12px;font-weight:600;padding:7px 13px;border-radius:8px;${i===0?'background:#047857;color:#fff':'background:#F1F5F9;color:#475569'}">${esc(a)}</span>`).join("")}</div>`;
let inner="";
if(s.type==="list"){
inner=`<div style="overflow-x:auto;-webkit-overflow-scrolling:touch"><table style="width:100%;border-collapse:collapse;font-size:12px;min-width:max-content"><tr>${(s.columns||[]).map(c=>`<th style="text-align:left;padding:7px 11px;background:#F5F6F8;border-bottom:2px solid #E5E7EB;font-size:10px;text-transform:uppercase;color:#9ca3af;letter-spacing:.04em;white-space:nowrap">${esc(c)}</th>`).join("")}</tr>${(s.rows||[]).map(r=>`<tr>${r.map(c=>`<td style="padding:8px 11px;border-bottom:1px solid #F1F5F9;white-space:nowrap">${esc(c)}</td>`).join("")}</tr>`).join("")}</table></div>`+acts;
}else if(s.type==="kanban"){
inner=`<div style="display:flex;gap:8px;overflow-x:auto">${(s.columns||[]).map(c=>`<div style="flex:1;min-width:120px;background:#F1F5F9;border-radius:8px;padding:8px"><div style="font-size:11px;font-weight:700;margin-bottom:6px">${esc(c)}</div><div style="background:#fff;border:1px solid #E5E7EB;border-radius:6px;padding:8px;font-size:11px">${(s.rows&&s.rows[0]&&s.rows[0][0])||'Карточка'}</div></div>`).join("")}</div>`+acts;
}else if(s.type==="dashboard"){
inner=`<div style="display:grid;grid-template-columns:repeat(${Math.min(4,(s.kpis||[]).length||1)},1fr);gap:10px">${(s.kpis||[]).map(k=>`<div style="background:#F5F6F8;border-radius:9px;padding:12px"><div style="font-size:22px;font-weight:800;font-family:Montserrat;color:#047857">${esc(k.value)}</div><div style="font-size:11px;color:#6B7280;margin-top:3px">${esc(k.label)}</div></div>`).join("")}</div>`+acts;
}else{ // form/card
inner=`<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">${(s.fields||[]).map(f=>`<div><div style="font-size:11px;font-weight:600;color:#6B7280;margin-bottom:4px">${esc(f.label)}</div><div style="border:1.5px solid #E5E7EB;border-radius:7px;padding:8px 11px;font-size:12px;color:#9ca3af;background:#FAFBFC">${f.kind==='select'?'▾ выбрать':f.kind==='status'?'● статус':f.kind==='money'?'0 ₽':f.kind==='date'?'дд.мм.гггг':'значение'}</div></div>`).join("")}</div>`+acts;
}
return frame(inner);
}
const TABS=[{id:"interview",name:"Интервью",icon:"💬"},{id:"methods",name:"Методологии",icon:"🎯"},{id:"canvas",name:"Стратегия",icon:"📊"},{id:"idef0",name:"Функции",icon:"🔧"},{id:"spec",name:"ТЗ",icon:"📋"}];
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 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 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 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");
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==="canvas"){if(!state.canvas){c.innerHTML=runCard("canvas","📊","Business Model Canvas","Стратегия — 9 блоков.","Построить →");return}c.innerHTML=renderCanvas(state.canvas)+cpBar("canvas","Стратегия верна?");}
else if(activeTab==="idef0"){if(!state.model){c.innerHTML=runCard("model","🔧","Функциональная модель IDEF0","Функции, входы/выходы, нормы, разрывы.","Построить →");return}c.innerHTML=renderIdef(state.model)+cpBar("idef0","Модель верна?");}
else if(activeTab==="spec"){if(!state.spec){if(!state.model){c.innerHTML=`<div class="run-card"><div class="run-ic">⚠️</div><div class="run-t">Сначала IDEF0</div><div class="run-d">ТЗ собирается из функциональной модели.</div></div>`;return}c.innerHTML=runCard("spec","📋","Техническое задание","Роли, модули, экраны, данные.","Собрать ТЗ →");return}c.innerHTML=`<div style="text-align:right;margin-bottom:12px"><button class="btn btn-p" onclick="exportSpecPDF()">⬇ Скачать ТЗ (PDF)</button></div>`+renderSpec(state.spec)+cpBar("spec","ТЗ готово к разработке?");}
}
const BUILD={methods:["select-methodologies","selection"],canvas:["build-canvas","canvas"],model:["build-model","model"],idef0:["build-model","model"],spec:["build-spec","spec"]};
async function rerun(stage){const [ep,key]=BUILD[stage];const btn=document.getElementById(`rb-${stage}`);if(btn){btn.disabled=true;btn.innerHTML='<span class="spin">⏳</span> Елена анализирует...'}if(approved(stage))unapprove(stage);
try{const r=await fetch(`${API}/api/${ep}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current})});const d=await r.json();if(d.error){alert("Ошибка: "+d.error);if(btn){btn.disabled=false;btn.textContent="Повторить"}return}state[key]=d[key];await loadProjects();renderClient();}catch(e){alert("Ошибка: "+e.message);if(btn)btn.disabled=false}}
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 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;}
loadProjects().then(render);
</script>
</body>
</html>