feat: PDF export of tech spec — branded printable document for developer

This commit is contained in:
wasrusgen 2026-05-30 13:58:36 +03:00
parent 4507625911
commit 2fab7092b3

View File

@ -214,6 +214,34 @@ async function saveCrm(){
}
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();}
@ -227,7 +255,7 @@ function renderTab(){const c=document.getElementById("tabContent");
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=renderSpec(state.spec)+cpBar("spec","ТЗ готово к разработке?");}
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);