wasrusgen1-crm/docs/crm.html

272 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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:14px;margin-bottom:22px}
.kpi{background:var(--white);border:1.5px solid var(--border);border-radius:14px;padding:20px}
.kpi-v{font-family:'Montserrat';font-weight:800;font-size:30px;color:var(--ink);letter-spacing:-1px}
.kpi-l{font-size:12px;color:var(--muted);margin-top:4px}
.kpi-sub{font-size:11px;color:var(--primary);font-weight:600;margin-top:6px}
.sec-h{font-family:'Montserrat';font-weight:800;font-size:18px;color:var(--ink);margin-bottom:14px}
/* 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)}
/* Tabs (analysis) */
.tabs{display:flex;gap:2px;border-bottom:1.5px solid var(--border);margin-bottom:18px}
.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";
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")+" ₽"}
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";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 revenue=projects.reduce((s,p)=>s+((p.crm&&p.crm.paid_amount)||0),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>
<div class="kpi"><div class="kpi-v">${active}</div><div class="kpi-l">Активных клиентов</div><div class="kpi-sub">${money(inwork)} в работе</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">${conv}%</div><div class="kpi-l">Конверсия в сделку</div><div class="kpi-sub">${done} завершено</div></div>
</div>
<div class="sec-h">Все клиенты · ${total}</div>
${projects.map(p=>{const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];return `<div class="blk" style="display:flex;align-items:center;gap:14px;cursor:pointer" onclick="openClient('${p.token}')"><div class="cl-av" style="width:36px;height:36px;border-radius:9px;font-size:14px">${esc((p.client_name||'?')[0])}</div><div style="flex:1"><div style="font-weight:700">${esc(p.client_name)}</div><div style="font-size:12px;color:var(--muted)">${esc(p.niche)} · ${p.msg_count} сообщений</div></div><span style="font-size:11px;font-weight:700;color:${pc[2]};background:${pc[2]}1a;padding:4px 10px;border-radius:20px">${pc[1]}</span><span style="font-weight:700;color:var(--primary)">${money((p.crm&&p.crm.deal_amount)||0)}</span></div>`}).join("")||'<div class="empty">Создайте первого клиента</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>`;
}
function renderClient(){
const crm=state.crm||{pipeline:"lead",deal_amount:0,paid_amount:0,contact:"",source:"",note:""};
document.getElementById("view").innerHTML=`
<div class="cc-top"><div class="cc-av">${esc((state.client_name||'?')[0])}</div><div><div class="cc-name">${esc(state.client_name||'Без имени')}</div><div class="cc-meta">${esc(state.niche||'')} · ${state.messages.length} сообщений</div></div></div>
<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="cpPaid" type="number" value="${crm.paid_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>
<div class="cc-actions"><button class="btn btn-p" onclick="inviteLink()">🔗 Ссылка клиенту</button><a class="btn btn-g" href="cabinet.html?t=${current}" target="_blank">👁 Открыть кабинет</a></div>
<div class="tabs">${TABS.map(t=>`<div class="tab ${t.id===activeTab?'active':''} ${approved(t.id)?'done':''}" onclick="setTab('${t.id}')">${t.icon} ${t.name}</div>`).join("")}</div>
<div id="tabContent"></div>`;
renderTab();
}
async function saveCrm(){
const crm={pipeline:document.getElementById("cpPipe").value,deal_amount:+document.getElementById("cpDeal").value||0,paid_amount:+document.getElementById("cpPaid").value||0,source:document.getElementById("cpSrc").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));}
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();
}
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=>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></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>`);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>