wasrusgen1-crm/docs/crm.html

377 lines
47 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: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 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()}
<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 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);
return `<div class="sec-h">📈 Выручка по месяцам · ${money(totalRev)}</div>
<div class="blk" style="padding:18px"><div style="display:flex;align-items:flex-end;gap:14px;height:140px">${keys.map(k=>`<div style="flex:1;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%;max-width:48px;background:linear-gradient(180deg,#10B981,#047857);border-radius:6px 6px 0 0;height:${Math.max(6,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);
const top=all.slice(0,6);
if(!top.length)return"";
return `<div class="sec-h">📌 Ближайшие задачи · ${all.length}</div>${top.map(t=>`<div class="blk" style="display:flex;align-items:center;gap:12px;cursor:pointer;padding:11px 14px" onclick="openClient('${t.token}')"><span style="font-size:13px;flex:1">${esc(t.text)}</span><span style="font-size:12px;color:var(--muted)">${esc(t.client)}</span>${t.due?`<span style="font-size:11px;font-weight:700;padding:3px 9px;border-radius:6px;${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("")}`;
}
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="cpSrc" value="${esc(crm.source)}" placeholder="откуда пришёл" onchange="saveCrm()"></div>
<div class="cc-field" id="payStatusBox"></div>
</div>
<div id="paymentsBox"></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><button class="btn btn-g" style="margin-left:auto;border-color:#FECACA;color:#DC2626" onclick="deleteClient()">🗑 Удалить клиента</button></div>
<div id="tasksBox"></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>`;
renderTasks();
renderPayments();
renderTab();
}
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"><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: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()"><input id="payNote" placeholder="Назначение (аванс, постоплата...)" style="flex:1;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})});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;state.crm=state.crm||{};state.crm.payments=state.crm.payments||[];state.crm.payments.push({date,amount:amt,note});renderPayments();savePayments();}
function delPayment(i){state.crm.payments.splice(i,1);renderPayments();savePayments();}
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();renderTasks();
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});renderTasks();saveTasks();}
function toggleTask(i){state.tasks[i].done=!state.tasks[i].done;renderTasks();saveTasks();}
function delTask(i){state.tasks.splice(i,1);renderTasks();saveTasks();}
async function saveCrm(){
const crm={pipeline:document.getElementById("cpPipe").value,deal_amount:+document.getElementById("cpDeal").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));}
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;max-width:640px"><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=`<table style="width:100%;border-collapse:collapse;font-size:12px"><tr>${(s.columns||[]).map(c=>`<th style="text-align:left;padding:7px 9px;background:#F5F6F8;border-bottom:2px solid #E5E7EB;font-size:10px;text-transform:uppercase;color:#9ca3af;letter-spacing:.04em">${esc(c)}</th>`).join("")}</tr>${(s.rows||[]).map(r=>`<tr>${r.map(c=>`<td style="padding:8px 9px;border-bottom:1px solid #F1F5F9">${esc(c)}</td>`).join("")}</tr>`).join("")}</table>`+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>