From 7b5162937d285378a2aa4eabe62d438d6031f41b Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Sat, 30 May 2026 16:13:19 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20payment=20gating=20=E2=80=94=20view=20a?= =?UTF-8?q?lways,=20print/download/export=20=D0=A2=D0=97=20unlock=20after?= =?UTF-8?q?=20debt=20closed=20(free=20clients=20always=20unlocked)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- Mokap/cabinet.html | 40 +++++++++++++++++++++++++++++++++++++++- backend/elena_app.py | 17 ++++++++++++++++- docs/cabinet.html | 40 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/Mokap/cabinet.html b/Mokap/cabinet.html index 3477742..96c6dad 100644 --- a/Mokap/cabinet.html +++ b/Mokap/cabinet.html @@ -163,6 +163,14 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ .pay-opt-t{font-size:15px;font-weight:700}.pay-opt-d{font-size:12px;color:var(--muted)} .modal-close{margin-top:8px;width:100%;background:transparent;border:none;color:var(--muted);font-size:13px;cursor:pointer;padding:8px} ::-webkit-scrollbar{width:6px}::-webkit-scrollbar-thumb{background:rgba(0,0,0,.12);border-radius:4px} +.btn-s{background:var(--white);color:var(--primary);border:1.5px solid var(--primary);font-size:13px;padding:9px 16px} +.btn-s:hover{background:var(--light)} +.exp-bar{margin-top:22px;border:1.5px solid var(--border);border-radius:16px;padding:22px;background:var(--white)} +.exp-bar.locked{background:linear-gradient(135deg,#FFFBEB,#FEF3C7);border-color:#FDE68A} +.exp-h{font-family:'Montserrat';font-weight:800;font-size:16px;color:var(--text);margin-bottom:6px} +.exp-d{font-size:13px;color:var(--muted);line-height:1.5;margin-bottom:14px} +.exp-btns{display:flex;flex-wrap:wrap;gap:10px} +@media print{.sb,.hdr,.pb,.hero,.nav,.exp-bar,.modal,.mic,.inp-bar{display:none!important}.main,.sv,.scroll,.pad{display:block!important;overflow:visible!important;height:auto!important;margin:0!important;padding:0!important}body{background:#fff}} @@ -496,8 +504,38 @@ function renderSpecPane(){ if(!state.spec){ if(!state.model){pad.innerHTML=runCard(null,"⚠️","Сначала Анализ","ТЗ собирается из функциональной модели. Перейдите на Этап 4 и постройте модель.","← К анализу",()=>go(4));return} pad.innerHTML=runCard("spec","📋","Техническое задание","Из модели бизнеса Елена спроектирует программу: роли, модули, экраны, данные.","Собрать ТЗ →");return} - pad.innerHTML=renderSpec(state.spec); + pad.innerHTML=renderSpec(state.spec)+renderExportBar(); } +function renderExportBar(){ + if(state.unlocked){ + return `
📦 Готовые документы
+
Долг закрыт — документы и ТЗ доступны для печати и выгрузки.
+
+ + + +
`; + } + const debt=state.debt||0, deal=state.deal_amount||0; + const money=deal>0?`Сумма: ${deal.toLocaleString('ru')} ₽${debt>0?` · к оплате: ${debt.toLocaleString('ru')} ₽`:''}`:''; + return `
+
🔒 Документы готовы — доступны после оплаты
+
Вы видите полный результат на экране. Печатный документ и выгрузка ТЗ для разработчика открываются после закрытия оплаты.
${money}
+
+
`; +} +function buildTZmd(s){ + let m=`# ТЗ · ${state.client_name||''}\n# @wasrusgen1 | КОНСАЛТИНГ\n\n## A. Обзор\n${s.overview||''}\n\n## A. Роли\n`; + (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((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 runCard(stage,ic,t,d,btn,custom){ const id=stage?`id="rb-${stage}"`:''; const onclick=custom?'':`onclick="runBuild('${stage}')"`; diff --git a/backend/elena_app.py b/backend/elena_app.py index 31b0d6b..a5ced4a 100644 --- a/backend/elena_app.py +++ b/backend/elena_app.py @@ -1058,6 +1058,16 @@ def get_project_state(token): model_row = db().execute( "SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (proj["id"],) ).fetchone() + crm = latest_artifact(proj["id"], "crm") or {"pipeline":"lead","deal_amount":0,"paid_amount":0,"contact":"","source":"","note":"","billing_type":"paid"} + payments = crm.get("payments", []) if isinstance(crm.get("payments"), list) else [] + paid_total = sum(p.get("amount", 0) for p in payments) if payments else (crm.get("paid_amount", 0) or 0) + deal_amount = crm.get("deal_amount", 0) or 0 + debt = max(0, deal_amount - paid_total) + billing = crm.get("billing_type", "paid") + # Разблокировка выгрузки документов/ТЗ: бесплатный клиент ИЛИ долг закрыт (платный с deal>0) + unlocked = (billing == "free") or (deal_amount > 0 and debt <= 0) + crm["paid_amount"] = paid_total + crm["debt"] = debt return jsonify({ "token": token, "client_name": proj["client_name"], @@ -1070,7 +1080,12 @@ def get_project_state(token): "canvas": latest_artifact(proj["id"], "canvas"), "spec": latest_artifact(proj["id"], "spec"), "approvals": latest_artifact(proj["id"], "approvals") or {}, - "crm": latest_artifact(proj["id"], "crm") or {"pipeline":"lead","deal_amount":0,"paid_amount":0,"contact":"","source":"","note":"","billing_type":"paid"}, + "crm": crm, + "unlocked": unlocked, + "debt": debt, + "paid_total": paid_total, + "deal_amount": deal_amount, + "billing_type": billing, "tasks": (latest_artifact(proj["id"], "tasks") or {}).get("items", []), "pricing": latest_artifact(proj["id"], "pricing"), "signed": db().execute("SELECT 1 FROM acceptances WHERE project_id=? AND doc='offer' LIMIT 1", (proj["id"],)).fetchone() is not None, diff --git a/docs/cabinet.html b/docs/cabinet.html index 3477742..96c6dad 100644 --- a/docs/cabinet.html +++ b/docs/cabinet.html @@ -163,6 +163,14 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ .pay-opt-t{font-size:15px;font-weight:700}.pay-opt-d{font-size:12px;color:var(--muted)} .modal-close{margin-top:8px;width:100%;background:transparent;border:none;color:var(--muted);font-size:13px;cursor:pointer;padding:8px} ::-webkit-scrollbar{width:6px}::-webkit-scrollbar-thumb{background:rgba(0,0,0,.12);border-radius:4px} +.btn-s{background:var(--white);color:var(--primary);border:1.5px solid var(--primary);font-size:13px;padding:9px 16px} +.btn-s:hover{background:var(--light)} +.exp-bar{margin-top:22px;border:1.5px solid var(--border);border-radius:16px;padding:22px;background:var(--white)} +.exp-bar.locked{background:linear-gradient(135deg,#FFFBEB,#FEF3C7);border-color:#FDE68A} +.exp-h{font-family:'Montserrat';font-weight:800;font-size:16px;color:var(--text);margin-bottom:6px} +.exp-d{font-size:13px;color:var(--muted);line-height:1.5;margin-bottom:14px} +.exp-btns{display:flex;flex-wrap:wrap;gap:10px} +@media print{.sb,.hdr,.pb,.hero,.nav,.exp-bar,.modal,.mic,.inp-bar{display:none!important}.main,.sv,.scroll,.pad{display:block!important;overflow:visible!important;height:auto!important;margin:0!important;padding:0!important}body{background:#fff}} @@ -496,8 +504,38 @@ function renderSpecPane(){ if(!state.spec){ if(!state.model){pad.innerHTML=runCard(null,"⚠️","Сначала Анализ","ТЗ собирается из функциональной модели. Перейдите на Этап 4 и постройте модель.","← К анализу",()=>go(4));return} pad.innerHTML=runCard("spec","📋","Техническое задание","Из модели бизнеса Елена спроектирует программу: роли, модули, экраны, данные.","Собрать ТЗ →");return} - pad.innerHTML=renderSpec(state.spec); + pad.innerHTML=renderSpec(state.spec)+renderExportBar(); } +function renderExportBar(){ + if(state.unlocked){ + return `
📦 Готовые документы
+
Долг закрыт — документы и ТЗ доступны для печати и выгрузки.
+
+ + + +
`; + } + const debt=state.debt||0, deal=state.deal_amount||0; + const money=deal>0?`Сумма: ${deal.toLocaleString('ru')} ₽${debt>0?` · к оплате: ${debt.toLocaleString('ru')} ₽`:''}`:''; + return `
+
🔒 Документы готовы — доступны после оплаты
+
Вы видите полный результат на экране. Печатный документ и выгрузка ТЗ для разработчика открываются после закрытия оплаты.
${money}
+
+
`; +} +function buildTZmd(s){ + let m=`# ТЗ · ${state.client_name||''}\n# @wasrusgen1 | КОНСАЛТИНГ\n\n## A. Обзор\n${s.overview||''}\n\n## A. Роли\n`; + (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((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 runCard(stage,ic,t,d,btn,custom){ const id=stage?`id="rb-${stage}"`:''; const onclick=custom?'':`onclick="runBuild('${stage}')"`;