diff --git a/Mokap/crm.html b/Mokap/crm.html
index 6ba7956..d2641a9 100644
--- a/Mokap/crm.html
+++ b/Mokap/crm.html
@@ -295,7 +295,7 @@ function renderMainPanel(){
renderPayments();
}
else if(mainTab==="pricing"){p.innerHTML=`
`;renderPricing();}
- else if(mainTab==="payments"){p.innerHTML=``;renderPayments();}
+ else if(mainTab==="payments"){p.innerHTML=``;renderPaymentPlan();renderPayments();}
else if(mainTab==="tasks"){p.innerHTML=``;renderTasks();}
else if(mainTab==="analysis"){p.innerHTML=`${TABS.map(t=>`
${t.icon} ${t.name}
`).join("")}
`;renderTab();}
}
@@ -305,6 +305,54 @@ async function setBilling(t){
await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,billing_type: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();
+}
+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);
+ 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());
+}
+function renderPaymentPlan(){
+ const box=document.getElementById("planBox");if(!box)return;
+ const sch=(state.crm&&state.crm.payment_schedule)||[];
+ if(!sch.length){
+ box.innerHTML=`💼 План оплаты
Выберите схему — система раскидает суммы от суммы сделки и привяжет к вехам анализа.
+
+
+
+
+
`;
+ return;
+ }
+ const total=sch.reduce((s,x)=>s+(x.amount||0),0);
+ const paid=sch.filter(x=>x.status==="paid").reduce((s,x)=>s+(x.amount||0),0);
+ box.innerHTML=`
+
💼 План оплатыОплачено ${money(paid)} из ${money(total)}
+ ${sch.map((s,i)=>{const isPaid=s.status==="paid";return `
+
${isPaid?'✓':i+1}
+
${esc(s.name)}
Веха: ${esc(trigName(s.trigger))}${isPaid&&s.paid_at?` · оплачен ${fmtDate(s.paid_at)}`:''}
+
${money(s.amount)}
+ ${isPaid?`
Оплачен`:`
`}
+
`}).join("")}
+
📄 ТЗ выдаётся клиенту (печать + выгрузка) после полной оплаты — финальный этап «Выдача ТЗ».
+
`;
+}
function renderPricing(){
const box=document.getElementById("pricingBox");if(!box)return;
const billing=(state.crm||{}).billing_type||"paid";
diff --git a/backend/elena_app.py b/backend/elena_app.py
index 567b173..a50c0d4 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"):
+ for k in ("pipeline", "deal_amount", "paid_amount", "contact", "source", "note", "payments", "billing_type", "payment_schedule"):
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 6ba7956..d2641a9 100644
--- a/docs/crm.html
+++ b/docs/crm.html
@@ -295,7 +295,7 @@ function renderMainPanel(){
renderPayments();
}
else if(mainTab==="pricing"){p.innerHTML=``;renderPricing();}
- else if(mainTab==="payments"){p.innerHTML=``;renderPayments();}
+ else if(mainTab==="payments"){p.innerHTML=``;renderPaymentPlan();renderPayments();}
else if(mainTab==="tasks"){p.innerHTML=``;renderTasks();}
else if(mainTab==="analysis"){p.innerHTML=`${TABS.map(t=>`
${t.icon} ${t.name}
`).join("")}
`;renderTab();}
}
@@ -305,6 +305,54 @@ async function setBilling(t){
await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,billing_type: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();
+}
+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);
+ 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());
+}
+function renderPaymentPlan(){
+ const box=document.getElementById("planBox");if(!box)return;
+ const sch=(state.crm&&state.crm.payment_schedule)||[];
+ if(!sch.length){
+ box.innerHTML=`💼 План оплаты
Выберите схему — система раскидает суммы от суммы сделки и привяжет к вехам анализа.
+
+
+
+
+
`;
+ return;
+ }
+ const total=sch.reduce((s,x)=>s+(x.amount||0),0);
+ const paid=sch.filter(x=>x.status==="paid").reduce((s,x)=>s+(x.amount||0),0);
+ box.innerHTML=`
+
💼 План оплатыОплачено ${money(paid)} из ${money(total)}
+ ${sch.map((s,i)=>{const isPaid=s.status==="paid";return `
+
${isPaid?'✓':i+1}
+
${esc(s.name)}
Веха: ${esc(trigName(s.trigger))}${isPaid&&s.paid_at?` · оплачен ${fmtDate(s.paid_at)}`:''}
+
${money(s.amount)}
+ ${isPaid?`
Оплачен`:`
`}
+
`}).join("")}
+
📄 ТЗ выдаётся клиенту (печать + выгрузка) после полной оплаты — финальный этап «Выдача ТЗ».
+
`;
+}
function renderPricing(){
const box=document.getElementById("pricingBox");if(!box)return;
const billing=(state.crm||{}).billing_type||"paid";