mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 21:04:47 +00:00
feat: payment gating — view always, print/download/export ТЗ unlock after debt closed (free clients always unlocked)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ebff789229
commit
7b5162937d
@ -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)}
|
.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}
|
.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}
|
::-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}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -496,8 +504,38 @@ function renderSpecPane(){
|
|||||||
if(!state.spec){
|
if(!state.spec){
|
||||||
if(!state.model){pad.innerHTML=runCard(null,"⚠️","Сначала Анализ","ТЗ собирается из функциональной модели. Перейдите на Этап 4 и постройте модель.","← К анализу",()=>go(4));return}
|
if(!state.model){pad.innerHTML=runCard(null,"⚠️","Сначала Анализ","ТЗ собирается из функциональной модели. Перейдите на Этап 4 и постройте модель.","← К анализу",()=>go(4));return}
|
||||||
pad.innerHTML=runCard("spec","📋","Техническое задание","Из модели бизнеса Елена спроектирует программу: роли, модули, экраны, данные.","Собрать ТЗ →");return}
|
pad.innerHTML=runCard("spec","📋","Техническое задание","Из модели бизнеса Елена спроектирует программу: роли, модули, экраны, данные.","Собрать ТЗ →");return}
|
||||||
pad.innerHTML=renderSpec(state.spec);
|
pad.innerHTML=renderSpec(state.spec)+renderExportBar();
|
||||||
}
|
}
|
||||||
|
function renderExportBar(){
|
||||||
|
if(state.unlocked){
|
||||||
|
return `<div class="exp-bar"><div class="exp-h">📦 Готовые документы</div>
|
||||||
|
<div class="exp-d">Долг закрыт — документы и ТЗ доступны для печати и выгрузки.</div>
|
||||||
|
<div class="exp-btns">
|
||||||
|
<button class="btn btn-p" onclick="printDoc()">🖨 Печать / PDF</button>
|
||||||
|
<button class="btn btn-s" onclick="downloadTZ()">⬇ Скачать ТЗ</button>
|
||||||
|
<button class="btn btn-s" onclick="exportDev()">{ } Выгрузить для разработчика</button>
|
||||||
|
</div></div>`;
|
||||||
|
}
|
||||||
|
const debt=state.debt||0, deal=state.deal_amount||0;
|
||||||
|
const money=deal>0?`Сумма: ${deal.toLocaleString('ru')} ₽${debt>0?` · к оплате: <b>${debt.toLocaleString('ru')} ₽</b>`:''}`:'';
|
||||||
|
return `<div class="exp-bar locked">
|
||||||
|
<div class="exp-h">🔒 Документы готовы — доступны после оплаты</div>
|
||||||
|
<div class="exp-d">Вы видите полный результат на экране. Печатный документ и выгрузка ТЗ для разработчика открываются после закрытия оплаты.<br>${money}</div>
|
||||||
|
<div class="exp-btns"><button class="btn btn-p" onclick="(window.showPayModal||function(){alert('Оплата скоро будет доступна')})()">Оплатить и забрать документы →</button></div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
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){
|
function runCard(stage,ic,t,d,btn,custom){
|
||||||
const id=stage?`id="rb-${stage}"`:'';
|
const id=stage?`id="rb-${stage}"`:'';
|
||||||
const onclick=custom?'':`onclick="runBuild('${stage}')"`;
|
const onclick=custom?'':`onclick="runBuild('${stage}')"`;
|
||||||
|
|||||||
@ -1058,6 +1058,16 @@ def get_project_state(token):
|
|||||||
model_row = db().execute(
|
model_row = db().execute(
|
||||||
"SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (proj["id"],)
|
"SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (proj["id"],)
|
||||||
).fetchone()
|
).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({
|
return jsonify({
|
||||||
"token": token,
|
"token": token,
|
||||||
"client_name": proj["client_name"],
|
"client_name": proj["client_name"],
|
||||||
@ -1070,7 +1080,12 @@ def get_project_state(token):
|
|||||||
"canvas": latest_artifact(proj["id"], "canvas"),
|
"canvas": latest_artifact(proj["id"], "canvas"),
|
||||||
"spec": latest_artifact(proj["id"], "spec"),
|
"spec": latest_artifact(proj["id"], "spec"),
|
||||||
"approvals": latest_artifact(proj["id"], "approvals") or {},
|
"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", []),
|
"tasks": (latest_artifact(proj["id"], "tasks") or {}).get("items", []),
|
||||||
"pricing": latest_artifact(proj["id"], "pricing"),
|
"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,
|
"signed": db().execute("SELECT 1 FROM acceptances WHERE project_id=? AND doc='offer' LIMIT 1", (proj["id"],)).fetchone() is not None,
|
||||||
|
|||||||
@ -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)}
|
.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}
|
.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}
|
::-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}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -496,8 +504,38 @@ function renderSpecPane(){
|
|||||||
if(!state.spec){
|
if(!state.spec){
|
||||||
if(!state.model){pad.innerHTML=runCard(null,"⚠️","Сначала Анализ","ТЗ собирается из функциональной модели. Перейдите на Этап 4 и постройте модель.","← К анализу",()=>go(4));return}
|
if(!state.model){pad.innerHTML=runCard(null,"⚠️","Сначала Анализ","ТЗ собирается из функциональной модели. Перейдите на Этап 4 и постройте модель.","← К анализу",()=>go(4));return}
|
||||||
pad.innerHTML=runCard("spec","📋","Техническое задание","Из модели бизнеса Елена спроектирует программу: роли, модули, экраны, данные.","Собрать ТЗ →");return}
|
pad.innerHTML=runCard("spec","📋","Техническое задание","Из модели бизнеса Елена спроектирует программу: роли, модули, экраны, данные.","Собрать ТЗ →");return}
|
||||||
pad.innerHTML=renderSpec(state.spec);
|
pad.innerHTML=renderSpec(state.spec)+renderExportBar();
|
||||||
}
|
}
|
||||||
|
function renderExportBar(){
|
||||||
|
if(state.unlocked){
|
||||||
|
return `<div class="exp-bar"><div class="exp-h">📦 Готовые документы</div>
|
||||||
|
<div class="exp-d">Долг закрыт — документы и ТЗ доступны для печати и выгрузки.</div>
|
||||||
|
<div class="exp-btns">
|
||||||
|
<button class="btn btn-p" onclick="printDoc()">🖨 Печать / PDF</button>
|
||||||
|
<button class="btn btn-s" onclick="downloadTZ()">⬇ Скачать ТЗ</button>
|
||||||
|
<button class="btn btn-s" onclick="exportDev()">{ } Выгрузить для разработчика</button>
|
||||||
|
</div></div>`;
|
||||||
|
}
|
||||||
|
const debt=state.debt||0, deal=state.deal_amount||0;
|
||||||
|
const money=deal>0?`Сумма: ${deal.toLocaleString('ru')} ₽${debt>0?` · к оплате: <b>${debt.toLocaleString('ru')} ₽</b>`:''}`:'';
|
||||||
|
return `<div class="exp-bar locked">
|
||||||
|
<div class="exp-h">🔒 Документы готовы — доступны после оплаты</div>
|
||||||
|
<div class="exp-d">Вы видите полный результат на экране. Печатный документ и выгрузка ТЗ для разработчика открываются после закрытия оплаты.<br>${money}</div>
|
||||||
|
<div class="exp-btns"><button class="btn btn-p" onclick="(window.showPayModal||function(){alert('Оплата скоро будет доступна')})()">Оплатить и забрать документы →</button></div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
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){
|
function runCard(stage,ic,t,d,btn,custom){
|
||||||
const id=stage?`id="rb-${stage}"`:'';
|
const id=stage?`id="rb-${stage}"`:'';
|
||||||
const onclick=custom?'':`onclick="runBuild('${stage}')"`;
|
const onclick=custom?'':`onclick="runBuild('${stage}')"`;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user