+ ${isUnlocked
+ ?`✅ Оплата получена. Клиент может скачать ТЗ и печатные документы.`
+ :`🔒 Правило: ТЗ (печать + выгрузка) выдаётся клиенту только после получения полной суммы — ${money(deal)}.`}
+
`;
}
function renderPricing(){
diff --git a/backend/elena_app.py b/backend/elena_app.py
index a50c0d4..d88ec76 100644
--- a/backend/elena_app.py
+++ b/backend/elena_app.py
@@ -645,7 +645,7 @@ def update_crm():
"pipeline": "lead", "deal_amount": 0, "paid_amount": 0,
"contact": "", "source": "", "note": "", "payments": [], "billing_type": "paid"
}
- for k in ("pipeline", "deal_amount", "paid_amount", "contact", "source", "note", "payments", "billing_type", "payment_schedule"):
+ for k in ("pipeline", "deal_amount", "paid_amount", "contact", "source", "note", "payments", "billing_type", "payment_schedule", "stage_payments"):
if k in data: crm[k] = data[k]
# paid_amount = сумма платежей (если есть реестр)
if "payments" in crm and isinstance(crm["payments"], list):
diff --git a/docs/crm.html b/docs/crm.html
index d2641a9..74881ea 100644
--- a/docs/crm.html
+++ b/docs/crm.html
@@ -306,51 +306,78 @@ async function setBilling(t){
renderClient();await loadProjects();
}
// ── План оплаты (привязка к вехам анализа) ──
-const PAY_TRIGGERS=[{key:"start",name:"Старт работы"},{key:"methods",name:"Согл. методологий"},{key:"canvas",name:"Согл. стратегии"},{key:"idef0",name:"Согл. модели IDEF0"},{key:"spec",name:"Выдача ТЗ"}];
-function trigName(k){const t=PAY_TRIGGERS.find(x=>x.key===k);return t?t.name:k;}
-function genSchedule(type){
- const deal=(state.crm&&state.crm.deal_amount)||0;
- if(!deal){alert("Сначала укажите сумму сделки (вкладка «Сделка» или «Ценообразование»).");return;}
- let arr;
- if(type==="full")arr=[{name:"Предоплата 100%",amount:deal,trigger:"start",status:"pending"}];
- else if(type==="half"){const a=Math.round(deal/2);arr=[{name:"Аванс 50%",amount:a,trigger:"start",status:"pending"},{name:"Постоплата 50%",amount:deal-a,trigger:"spec",status:"pending"}];}
- else{const a=Math.round(deal*0.4),b=Math.round(deal*0.3);arr=[{name:"Аванс 40%",amount:a,trigger:"start",status:"pending"},{name:"Промежуточный 30%",amount:b,trigger:"idef0",status:"pending"},{name:"Финал 30%",amount:deal-a-b,trigger:"spec",status:"pending"}];}
- arr.forEach((s,i)=>s.id="s"+i);
- state.crm=state.crm||{};state.crm.payment_schedule=arr;
- saveSchedule();renderClient();
+// ── Этапы оплаты — от прогресса клиента ──────────────────
+// Этапы = 5 этапов анализа. Отмечаем оплату по мере прохождения.
+// Правило: ТЗ (печать + выгрузка) только после 100% оплаты.
+const CLIENT_STAGES=[
+ {key:"interview", name:"Интервью", icon:"💬"},
+ {key:"methods", name:"Методологии", icon:"🎯"},
+ {key:"canvas", name:"Стратегия", icon:"📊"},
+ {key:"idef0", name:"Функции IDEF0", icon:"🔧"},
+ {key:"spec", name:"Выдача ТЗ", icon:"📋"},
+];
+function stagePayKey(k){return "pay_"+k;}
+function stageIsDone(k){
+ if(k==="interview") return (state.messages||[]).length>0;
+ if(k==="methods") return !!state.selection;
+ if(k==="canvas") return !!state.canvas;
+ if(k==="idef0") return !!(state.model);
+ if(k==="spec") return !!state.spec;
+ return false;
}
-async function saveSchedule(){await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,payment_schedule:state.crm.payment_schedule})});await loadProjects();}
-function clearSchedule(){if(!confirm("Сменить схему плана оплаты? Реестр фактических платежей не затрагивается."))return;state.crm.payment_schedule=[];saveSchedule();renderClient();}
-function markStagePaid(i){
- const s=state.crm.payment_schedule[i];if(!s||s.status==="paid")return;
- s.status="paid";s.paid_at=new Date().toISOString().slice(0,10);
+function getStagePays(){return (state.crm&&state.crm.stage_payments)||{};}
+async function markStagePaid(k){
+ const deal=(state.crm&&state.crm.deal_amount)||0;
+ const pays=getStagePays();
+ if(pays[k])return;
+ const amt=prompt(`Сумма оплаты за этап «${CLIENT_STAGES.find(s=>s.key===k).name}» (₽):`,deal>0?Math.round(deal/5):"");
+ if(!amt||isNaN(+amt))return;
+ const today=new Date().toISOString().slice(0,10);
+ pays[k]={amount:+amt,date:today};
+ state.crm=state.crm||{};
+ state.crm.stage_payments=pays;
state.crm.payments=state.crm.payments||[];
- state.crm.payments.push({date:s.paid_at,amount:s.amount,note:"Этап: "+s.name});
- Promise.all([saveSchedule(),savePayments()]).then(()=>renderClient());
+ state.crm.payments.push({date:today,amount:+amt,note:"Этап: "+CLIENT_STAGES.find(s=>s.key===k).name});
+ await Promise.all([
+ fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage_payments:pays,payments:state.crm.payments})}),
+ loadProjects()
+ ]);
+ renderClient();
}
function renderPaymentPlan(){
const box=document.getElementById("planBox");if(!box)return;
- const sch=(state.crm&&state.crm.payment_schedule)||[];
- if(!sch.length){
- box.innerHTML=`
💼 План оплаты
Выберите схему — система раскидает суммы от суммы сделки и привяжет к вехам анализа.
+ ${isUnlocked
+ ?`✅ Оплата получена. Клиент может скачать ТЗ и печатные документы.`
+ :`🔒 Правило: ТЗ (печать + выгрузка) выдаётся клиенту только после получения полной суммы — ${money(deal)}.`}
+