From 5e67ddb286ad8e28224276fa81ba55173fc396c4 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Tue, 2 Jun 2026 00:14:52 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BE=D1=80=D0=B3.=D1=81=D0=BB=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B2=20CRM=20+=20=D0=9E=D1=80=D0=B3=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B2=20=D1=81=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D0=B5=20+=20Phase=203=20(=D0=A2=D0=97=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=B0)=20+?= =?UTF-8?q?=20=D0=B2=D1=8B=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Операторская CRM: вкладка «Организация» (оргструктура+должностные) с пересборкой 2. Смета: «Организация» — оплачиваемый этап (org:10000) во всех массивах стадий 3. Phase 3: /api/build-spec-client — ТЗ под вариант клиента (учёт отклонений+орг), переключатель «Эталон Елены / Вариант клиента» в ТЗ-вкладке CRM 4. Должностные+оргструктура в выгрузку ТЗ (PDF в CRM, MD+JSON в кабинете) --- backend/elena_app.py | 38 ++++++++++++++++ docs/cabinet.html | 6 ++- docs/crm.html | 104 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 134 insertions(+), 14 deletions(-) diff --git a/backend/elena_app.py b/backend/elena_app.py index 713682d..8f7764d 100644 --- a/backend/elena_app.py +++ b/backend/elena_app.py @@ -877,6 +877,42 @@ def build_spec(): con = db(); con.execute("UPDATE projects SET status='spec_ready' WHERE id=?", (proj["id"],)); con.commit() return jsonify({"spec": result, "usage": usage}) +@app.route("/api/build-spec-client", methods=["POST"]) +def build_spec_client(): + """ТЗ под вариант клиента: полная пересборка с учётом зафиксированных отклонений и орг. слоя.""" + data = request.get_json(force=True) or {} + proj = get_project(data.get("token")) + if not proj: + return jsonify({"error": "project not found"}), 404 + pid = proj["id"] + model_row = db().execute("SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (pid,)).fetchone() + if not model_row: + return jsonify({"error": "сначала постройте IDEF0 модель"}), 400 + devs = get_deviations(pid) + if not devs: + return jsonify({"error": "нет зафиксированных отклонений — вариант клиента совпадает с эталоном"}), 400 + idef0 = model_row["blocks_json"] + extra = [] + org = latest_artifact(pid, "orgchart") + if org: + extra.append("ЦЕЛЕВАЯ ОРГСТРУКТУРА:\n" + json.dumps(org, ensure_ascii=False)[:3000]) + jobs = latest_artifact(pid, "jobs") + if jobs: + extra.append("ДОЛЖНОСТНЫЕ ИНСТРУКЦИИ:\n" + json.dumps(jobs, ensure_ascii=False)[:3000]) + extra.append("ЗАФИКСИРОВАННЫЕ ОТКЛОНЕНИЯ КЛИЕНТА (реализуем ИХ вариант, не эталон):\n" + json.dumps(devs, ensure_ascii=False)[:3000]) + instruction = ("Собери ТЗ на программу под РЕАЛЬНЫЙ вариант клиента (с его отклонениями), а не под методологический эталон.\n" + "Базис — интервью и IDEF0, НО там, где клиент настоял на своём (см. отклонения) — проектируй под выбор клиента.\n" + "Например, если клиент оставил совмещение склада и пошива — модули и роли должны это отражать (один человек, общий доступ), " + "но в open_questions честно вынеси риски, о которых предупреждала Елена.\n" + "МАППИНГ: функция → модуль; Input → данные; Output → показ; Control → правила; Mechanism → роли; хранилища → таблицы.\n" + "Думай как проектировщик ПО. Вызови build_tech_spec.\n\n" + f"IDEF0-МОДЕЛЬ:\n{idef0}\n\n" + "\n\n".join(extra)) + result, usage = run_tool(pid, SPEC_TOOL, "build_tech_spec", instruction, max_tokens=8192) + if result is None: + return jsonify({"error": usage}), 500 + save_artifact(pid, "spec_client", result) + return jsonify({"spec_client": result, "usage": usage}) + @app.route("/api/project/crm", methods=["POST"]) def update_crm(): data = request.get_json(force=True) or {} @@ -1298,6 +1334,7 @@ def list_projects(): "has_selection": latest_artifact(pid, "selection") is not None, "has_canvas": latest_artifact(pid, "canvas") is not None, "has_idef0": db().execute("SELECT 1 FROM models WHERE project_id=? LIMIT 1", (pid,)).fetchone() is not None, + "has_org": latest_artifact(pid, "orgchart") is not None, "has_spec": latest_artifact(pid, "spec") is not None, "approvals": latest_artifact(pid, "approvals") or {}, "crm": latest_artifact(pid, "crm") or {"pipeline":"lead","deal_amount":0,"paid_amount":0,"contact":"","source":"","note":""}, @@ -1434,6 +1471,7 @@ def get_project_state(token): "orgchart": latest_artifact(proj["id"], "orgchart"), "jobs": latest_artifact(proj["id"], "jobs"), "spec": latest_artifact(proj["id"], "spec"), + "spec_client": latest_artifact(proj["id"], "spec_client"), "approvals": latest_artifact(proj["id"], "approvals") or {}, "crm": crm, "unlocked": unlocked, diff --git a/docs/cabinet.html b/docs/cabinet.html index 58be971..bbbbd7f 100644 --- a/docs/cabinet.html +++ b/docs/cabinet.html @@ -665,6 +665,7 @@ const EST_STAGES=[ {key:"methods", name:"Методологии", icon:"🎯", desc:"Подбор фреймворков под вашу задачу"}, {key:"canvas", name:"Стратегия", icon:"📊", desc:"Целевая модель процессов (TO-BE)"}, {key:"idef0", name:"Функции IDEF0", icon:"🔧", desc:"Декомпозиция функций + регламенты"}, + {key:"org", name:"Организация", icon:"🏢", desc:"Оргструктура + должностные инструкции"}, {key:"spec", name:"Выдача ТЗ", icon:"📋", desc:"Готовое ТЗ к внедрению"}, ]; function estStageDone(k){ @@ -672,6 +673,7 @@ function estStageDone(k){ if(k==="methods") return !!state.selection; if(k==="canvas") return !!state.canvas; if(k==="idef0") return !!state.model; + if(k==="org") return !!(state.orgchart && state.jobs); if(k==="spec") return !!state.spec; return false; } @@ -732,13 +734,15 @@ function buildTZmd(s){ (s.roles||[]).forEach(r=>m+=`- **${r.name}** — ${r.does} (доступ: ${r.access})\n`); m+=`\n## B. Модули\n`;(s.modules||[]).forEach(x=>{m+=`### ${x.name} [${x.source_node}]\n${x.purpose}\nЭкраны: ${(x.screens||[]).join(', ')}\nДанные: вход — ${x.inputs_data}; выход — ${x.outputs_data}\n`;(x.rules||[]).forEach(r=>m+=`- правило: ${r}\n`);m+=`\n`}); m+=`## C. Модель данных\n`;(s.entities||[]).forEach(e=>{m+=`### ${e.name}\n`;(e.fields||[]).forEach(f=>m+=`- ${f.field}: ${f.type}\n`);if((e.relations||[]).length)m+=`Связи: ${e.relations.join(' · ')}\n`;m+=`Пример: ${e.example}\n\n`}); + if(state.orgchart&&(state.orgchart.units||[]).length){m+=`\n## D. Оргструктура\n${state.orgchart.insight||''}\n`;state.orgchart.units.forEach(u=>{m+=`- **${u.role}** (${(+u.headcount||1)} чел.)${u.reports_to&&u.reports_to!=='—'?` ↑ ${u.reports_to}`:''}${(u.owns_functions||[]).length?` — отвечает: ${u.owns_functions.join(', ')}`:''}${u.note?` ⚠ ${u.note}`:''}\n`})} + if(state.jobs&&(state.jobs.roles||[]).length){m+=`\n## E. Должностные инструкции\n`;state.jobs.roles.forEach(r=>{m+=`### ${r.role}\n${r.purpose||''}${r.reports_to?` (подчинение: ${r.reports_to})`:''}\n`;(r.responsibilities||[]).forEach(x=>m+=`- ${x}\n`);if((r.kpis||[]).length)m+=`KPI: ${r.kpis.join(' · ')}\n`;if((r.authority||[]).length)m+=`Полномочия: ${r.authority.join(' · ')}\n`;if(r.deviation_note)m+=`⚠ Учтено пожелание клиента: ${r.deviation_note}\n`;m+=`\n`})} if((s.open_questions||[]).length){m+=`## Уточнить перед разработкой\n`;s.open_questions.forEach(q=>m+=`- ${q}\n`)} return m; } function dl(name,text,type){const b=new Blob([text],{type:type||'text/plain;charset=utf-8'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),2000)} function printDoc(){if(!state.unlocked)return alert('Доступно после оплаты');window.print()} function downloadTZ(){if(!state.unlocked)return alert('Доступно после оплаты');dl(`ТЗ_${(state.client_name||'проект').replace(/\s+/g,'_')}.md`,buildTZmd(state.spec),'text/markdown;charset=utf-8')} -function exportDev(){if(!state.unlocked)return alert('Доступно после оплаты');dl(`ТЗ_${(state.client_name||'проект').replace(/\s+/g,'_')}.json`,JSON.stringify({client:state.client_name,niche:state.niche,model:state.model,spec:state.spec},null,2),'application/json')} +function exportDev(){if(!state.unlocked)return alert('Доступно после оплаты');dl(`ТЗ_${(state.client_name||'проект').replace(/\s+/g,'_')}.json`,JSON.stringify({client:state.client_name,niche:state.niche,model:state.model,orgchart:state.orgchart,jobs:state.jobs,spec:state.spec},null,2),'application/json')} function runCard(stage,ic,t,d,btn,custom){ const id=stage?`id="rb-${stage}"`:''; const onclick=custom?'':`onclick="runBuild('${stage}')"`; diff --git a/docs/crm.html b/docs/crm.html index 885e726..222e627 100644 --- a/docs/crm.html +++ b/docs/crm.html @@ -199,7 +199,7 @@ function setClientFilter(f){ui.clientFilter=f;saveUi();renderDashboard();} function setTaskFilter(f){ui.taskFilter=f;saveUi();renderDashboard();} function setClientSearch(v){ui.clientSearch=v;renderDashboard();const el=document.getElementById('clientSearch');if(el){el.focus();const L=el.value.length;try{el.setSelectionRange(L,L);}catch(e){}}} function setClientSort(k){if(ui.clientSort===k)ui.clientSortDir=(ui.clientSortDir||1)*-1;else{ui.clientSort=k;ui.clientSortDir=1;}saveUi();renderDashboard();} -function projStageCnt(p){return [p.msg_count>0,!!p.has_selection,!!p.has_canvas,!!p.has_idef0,!!p.has_spec].filter(Boolean).length;} +function projStageCnt(p){return [p.msg_count>0,!!p.has_selection,!!p.has_canvas,!!p.has_idef0,!!p.has_org,!!p.has_spec].filter(Boolean).length;} function secHead(k,title,right){ const col=!!ui.collapsed[k]; return `
@@ -289,9 +289,9 @@ function renderDashboard(){ return head+bar+`
${cl.map(p=>renderClientRow(p)).join("")}
`; })()}`; } -const STAGE_DEFS=[{key:"interview",name:"Интервью"},{key:"methods",name:"Методологии"},{key:"canvas",name:"Стратегия"},{key:"idef0",name:"Функции"},{key:"spec",name:"ТЗ"}]; +const STAGE_DEFS=[{key:"interview",name:"Интервью"},{key:"methods",name:"Методологии"},{key:"canvas",name:"Стратегия"},{key:"idef0",name:"Функции"},{key:"org",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 done=[p.msg_count>0,!!p.has_selection,!!p.has_canvas,!!p.has_idef0,!!p.has_org,!!p.has_spec]; const cnt=done.filter(Boolean).length; let cur=done.findIndex(d=>!d); // первый незавершённый const all=cur===-1; @@ -532,10 +532,11 @@ const CLIENT_STAGES=[ {key:"methods", name:"Методологии", icon:"🎯", desc:"Подбор фреймворков под вашу задачу"}, {key:"canvas", name:"Стратегия", icon:"📊", desc:"Целевая модель процессов (TO-BE)"}, {key:"idef0", name:"Функции IDEF0", icon:"🔧", desc:"Декомпозиция функций + регламенты"}, + {key:"org", name:"Организация", icon:"🏢", desc:"Оргструктура + должностные инструкции"}, {key:"spec", name:"Выдача ТЗ", icon:"📋", desc:"Готовое ТЗ к внедрению"}, ]; // ── Смета: базовые ставки модулей (интервью = вход 0₽) ── -const STAGE_BASE={interview:0, methods:8000, canvas:12000, idef0:15000, spec:5000}; +const STAGE_BASE={interview:0, methods:8000, canvas:12000, idef0:15000, org:10000, spec:5000}; const CX_COEF={low:1.0, medium:1.5, high:2.0}; const CX_LABEL={low:"Простой · 1–3 процесса", medium:"Средний · 4–7", high:"Сложный · 8+"}; const CX_SHORT={low:"Простой ×1.0", medium:"Средний ×1.5", high:"Сложный ×2.0"}; @@ -617,6 +618,7 @@ function stageIsDone(k){ if(k==="methods") return !!state.selection; if(k==="canvas") return !!state.canvas; if(k==="idef0") return !!(state.model); + if(k==="org") return !!(state.orgchart && state.jobs); if(k==="spec") return !!state.spec; return false; } @@ -955,14 +957,20 @@ async function deleteClient(){ } function exportSpecPDF(){ - const s=state.spec;if(!s){alert("ТЗ ещё не собрано");return} + const isClient=(specVariant==="client"&&state.spec_client); + const s=isClient?state.spec_client:state.spec;if(!s){alert("ТЗ ещё не собрано");return} const cn=esc(state.client_name||"Клиент"),nm=esc(state.niche||""); const w=window.open("","_blank"); - let body=`

1. Обзор системы

${esc(s.overview)}

`; - body+=`

2. Роли пользователей

`;s.roles.forEach(r=>body+=`
${esc(r.name)} — ${esc(r.does)}
Доступ: ${esc(r.access)}
`); - body+=`

3. Модули системы

`;s.modules.forEach(m=>{body+=`
${esc(m.source_node)} ${esc(m.name)}
${esc(m.purpose)}
Экраны: ${m.screens.map(esc).join(", ")}
Вход: ${esc(m.inputs_data)}
Выход: ${esc(m.outputs_data)}
${m.rules.length?`
Бизнес-правила:
    ${m.rules.map(r=>`
  • ${esc(r)}
  • `).join("")}
`:''}
`}); - body+=`

4. Модель данных

`;s.entities.forEach(e=>{body+=`
◆ ${esc(e.name)}${e.fields.map(f=>``).join("")}
ПолеТип
${esc(f.field)}${esc(f.type)}
${e.relations.length?`
Связи: ${e.relations.map(esc).join("; ")}
`:''}
${esc(e.example)}
`}); - if(s.open_questions&&s.open_questions.length){body+=`

5. Уточнить перед разработкой

`} + let n=0;const H=t=>`

${++n}. ${t}

`; + let body=H("Обзор системы")+`

${esc(s.overview)}

`; + body+=H("Роли пользователей");s.roles.forEach(r=>body+=`
${esc(r.name)} — ${esc(r.does)}
Доступ: ${esc(r.access)}
`); + body+=H("Модули системы");s.modules.forEach(m=>{body+=`
${esc(m.source_node)} ${esc(m.name)}
${esc(m.purpose)}
Экраны: ${m.screens.map(esc).join(", ")}
Вход: ${esc(m.inputs_data)}
Выход: ${esc(m.outputs_data)}
${m.rules.length?`
Бизнес-правила:
    ${m.rules.map(r=>`
  • ${esc(r)}
  • `).join("")}
`:''}
`}); + body+=H("Модель данных");s.entities.forEach(e=>{body+=`
◆ ${esc(e.name)}${e.fields.map(f=>``).join("")}
ПолеТип
${esc(f.field)}${esc(f.type)}
${e.relations.length?`
Связи: ${e.relations.map(esc).join("; ")}
`:''}
${esc(e.example)}
`}); + // Оргструктура + if(state.orgchart&&(state.orgchart.units||[]).length){body+=H("Оргструктура");body+=`
${esc(state.orgchart.insight||'')}
`;state.orgchart.units.forEach(u=>{body+=`
${esc(u.role)} ${(+u.headcount||1)} чел.${u.reports_to&&u.reports_to!=='—'?` ↑ ${esc(u.reports_to)}`:''}${(u.owns_functions&&u.owns_functions.length)?`
Отвечает: ${u.owns_functions.map(esc).join(", ")}
`:''}${u.note?`
⚠ ${esc(u.note)}
`:''}
`});} + // Должностные инструкции + if(state.jobs&&(state.jobs.roles||[]).length){body+=H("Должностные инструкции");state.jobs.roles.forEach(r=>{body+=`
${esc(r.role)}
${esc(r.purpose||'')}${r.reports_to?` · ↑ ${esc(r.reports_to)}`:''}
${(r.responsibilities&&r.responsibilities.length)?`
Ответственность:
    ${r.responsibilities.map(x=>`
  • ${esc(x)}
  • `).join("")}
`:''}${(r.kpis&&r.kpis.length)?`
KPI: ${r.kpis.map(esc).join(" · ")}
`:''}${(r.authority&&r.authority.length)?`
Полномочия: ${r.authority.map(esc).join(" · ")}
`:''}${r.deviation_note?`
⚠ Учтено пожелание клиента: ${esc(r.deviation_note)}
`:''}
`});} + if(s.open_questions&&s.open_questions.length){body+=H("Уточнить перед разработкой")+``} w.document.write(`ТЗ — ${cn}