`;
}
const debt=state.debt||0, deal=state.deal_amount||0;
- const money=deal>0?`Сумма: ${deal.toLocaleString('ru')} ₽${debt>0?` · к оплате:
+ const moneyStr=deal>0?`Сумма: ${deal.toLocaleString('ru')} ₽${debt>0?` · к оплате:
${debt.toLocaleString('ru')} ₽ `:''}`:'';
+ return renderEstimateCard()+`
🔒 Документы готовы — доступны после оплаты
-
Вы видите полный результат на экране. Печатный документ и выгрузка ТЗ для разработчика открываются после закрытия оплаты. ${money}
+
Вы видите полный результат на экране. Печатный документ и выгрузка ТЗ для разработчика открываются после закрытия оплаты. ${moneyStr}
Оплатить и забрать документы →
`;
}
diff --git a/docs/crm.html b/docs/crm.html
index 74881ea..89919f4 100644
--- a/docs/crm.html
+++ b/docs/crm.html
@@ -310,12 +310,51 @@ async function setBilling(t){
// Этапы = 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:"📋"},
+ {key:"interview", name:"Интервью", icon:"💬", desc:"Диагностика «как есть» — карта проблем"},
+ {key:"methods", name:"Методологии", icon:"🎯", desc:"Подбор фреймворков под вашу задачу"},
+ {key:"canvas", name:"Стратегия", icon:"📊", desc:"Целевая модель процессов (TO-BE)"},
+ {key:"idef0", name:"Функции IDEF0", icon:"🔧", desc:"Декомпозиция функций + регламенты"},
+ {key:"spec", name:"Выдача ТЗ", icon:"📋", desc:"Готовое ТЗ к внедрению"},
];
+// ── Смета: базовые ставки модулей (интервью = вход 0₽) ──
+const STAGE_BASE={interview:0, methods:8000, canvas:12000, idef0:15000, 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"};
+function getStagePrices(){return (state.crm&&state.crm.stage_prices)||null;}
+function getComplexity(){return (state.crm&&state.crm.complexity)||"medium";}
+function recalcDeal(sp){return Object.values(sp).reduce((a,b)=>a+(+b||0),0);}
+function buildEstimate(){
+ const coef=CX_COEF[getComplexity()];
+ const sp={};
+ CLIENT_STAGES.forEach(s=>{sp[s.key]=Math.round((STAGE_BASE[s.key]||0)*coef/100)*100;});
+ state.crm=state.crm||{};
+ state.crm.stage_prices=sp;
+ state.crm.complexity=getComplexity();
+ state.crm.deal_amount=recalcDeal(sp);
+ saveEstimate();renderClient();
+}
+async function setComplexity(cx){
+ state.crm=state.crm||{};state.crm.complexity=cx;
+ if(state.crm.stage_prices){buildEstimate();return;}
+ await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,complexity:cx})});
+ renderClient();
+}
+function editStagePrice(k){
+ const cur=(getStagePrices()||{})[k]||0;
+ const v=prompt(`Цена этапа «${CLIENT_STAGES.find(s=>s.key===k).name}» (₽):`,cur);
+ if(v===null)return;
+ const n=Math.max(0,Math.round(+v||0));
+ state.crm=state.crm||{};
+ state.crm.stage_prices=state.crm.stage_prices||{};
+ state.crm.stage_prices[k]=n;
+ state.crm.deal_amount=recalcDeal(state.crm.stage_prices);
+ saveEstimate();renderClient();
+}
+async function saveEstimate(){
+ await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage_prices:state.crm.stage_prices,complexity:state.crm.complexity,deal_amount:state.crm.deal_amount})});
+ await loadProjects();
+}
function stagePayKey(k){return "pay_"+k;}
function stageIsDone(k){
if(k==="interview") return (state.messages||[]).length>0;
@@ -326,18 +365,32 @@ function stageIsDone(k){
return false;
}
function getStagePays(){return (state.crm&&state.crm.stage_payments)||{};}
-async function markStagePaid(k){
+function markStagePayInput(k){
+ const fid='spf-'+k;
+ const ex=document.getElementById(fid);if(ex){ex.remove();return;}
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 sp=getStagePrices()||{};
+ const suggested=sp[k]||(deal>0?Math.round(deal/5):'');
const today=new Date().toISOString().slice(0,10);
- pays[k]={amount:+amt,date:today};
+ const wrap=document.createElement('div');
+ wrap.id=fid;
+ wrap.style.cssText='display:flex;gap:8px;margin-top:8px;flex-wrap:wrap;align-items:center;padding:10px 12px;background:var(--bg);border-radius:10px;border:1.5px solid #D1FAE5';
+ wrap.innerHTML=`
Сохранить ✕ `;
+ const row=document.getElementById('stagerow-'+k);
+ if(row)row.parentNode.insertBefore(wrap,row.nextSibling);
+}
+async function confirmStagePay(k){
+ const amt=+document.getElementById('spa-'+k).value;
+ const date=document.getElementById('spd-'+k).value||new Date().toISOString().slice(0,10);
+ const note=document.getElementById('spn-'+k).value;
+ if(!amt){alert('Укажите сумму');return;}
+ const pays=getStagePays();if(pays[k])return;
+ const stageName=CLIENT_STAGES.find(s=>s.key===k).name;
+ pays[k]={amount:amt,date};
state.crm=state.crm||{};
state.crm.stage_payments=pays;
state.crm.payments=state.crm.payments||[];
- state.crm.payments.push({date:today,amount:+amt,note:"Этап: "+CLIENT_STAGES.find(s=>s.key===k).name});
+ state.crm.payments.push({date,amount:amt,note:note||'Этап: '+stageName,stage:k});
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()
@@ -346,37 +399,78 @@ async function markStagePaid(k){
}
function renderPaymentPlan(){
const box=document.getElementById("planBox");if(!box)return;
- const deal=(state.crm&&state.crm.deal_amount)||0;
+ const billing=(state.crm&&state.crm.billing_type)||"paid";
+ const sp=getStagePrices();
const pays=getStagePays();
const paidTotal=Object.values(pays).reduce((s,p)=>s+(p.amount||0),0);
- const billing=(state.crm&&state.crm.billing_type)||"paid";
- const isUnlocked=billing==="free"||(deal>0&&paidTotal>=deal);
+
+ // ── Сметы ещё нет → генератор ──
+ if(!sp){
+ const cx=getComplexity();
+ box.innerHTML=`
+
📋 Смета проекта
+
После диагностики сформируйте смету. Клиент увидит, за что и сколько платит. Интервью — бесплатный вход, дальше оплата помодульно. ТЗ выдаётся после полной оплаты.
+
Сложность проекта (из интервью):
+
+ ${Object.keys(CX_COEF).map(k=>`${CX_LABEL[k]} `).join('')}
+
+
Предварительно: ${money(recalcDeal(Object.fromEntries(CLIENT_STAGES.map(s=>[s.key,Math.round((STAGE_BASE[s.key]||0)*CX_COEF[cx]/100)*100]))))} за проект
+
Сформировать смету →
+
`;
+ return;
+ }
+
+ const total=recalcDeal(sp);
+ const isUnlocked=billing==="free"||(total>0&&paidTotal>=total);
+ const cx=getComplexity();
+ // первый ещё не оплаченный платный выполненный модуль — «доступен к оплате»
box.innerHTML=`
-
-
💼 Прогресс оплаты по этапам
- ${deal>0?`
Получено ${money(paidTotal)} из ${money(deal)} `:'
⚠ Укажите сумму сделки во вкладке «Сделка» '}
+
+ 📋 Смета проекта
+ ${CX_SHORT[cx]}
+ ↻ Пересчитать
+
Получено ${money(paidTotal)} из ${money(total)} · нажмите цену чтобы изменить
${CLIENT_STAGES.map(s=>{
const done=stageIsDone(s.key);
const paid=pays[s.key];
- return `
-
${s.icon}
+ const price=sp[s.key]||0;
+ const isFree=price<=0;
+ let badge='', sub='', action='';
+ if(isFree){
+ badge=`
Вход · бесплатно `;
+ sub=`
${esc(s.desc)}
`;
+ action=`
0 ₽ `;
+ } else if(paid){
+ badge=`
✓ Оплачен `;
+ sub=`
Оплачено ${money(paid.amount)} · ${fmtDate(paid.date)}
`;
+ action=`
${money(price)} `;
+ } else if(done){
+ badge=`
Доступен к оплате `;
+ sub=`
${esc(s.desc)}
`;
+ action=`
Оплата · ${money(price)} `;
+ } else {
+ badge=`
🔒 Ожидает `;
+ sub=`
${esc(s.desc)}
`;
+ action=`
${money(price)} `;
+ }
+ return `
+
${s.icon}
-
${s.name}${done?' ✓ Выполнен ':''}
- ${paid?`
Оплачено ${money(paid.amount)} · ${fmtDate(paid.date)}
`:done?`
Этап выполнен — можно запросить оплату
`:`
Ожидается выполнение этапа
`}
+
${s.name}${badge}
+ ${sub}
- ${paid
- ?`
${money(paid.amount)} `
- :done
- ?`
Отметить оплату `
- :`
— `
- }
+ ${action}
`;
}).join("")}
+
+ Итого по проекту
+ ${money(total)}
+
${isUnlocked
?`✅ Оплата получена. Клиент может скачать ТЗ и печатные документы.`
- :`🔒 Правило: ТЗ (печать + выгрузка) выдаётся клиенту только после получения полной суммы — ${money(deal)}.`}
+ :`🔒 Правило: ТЗ (печать + выгрузка) выдаётся клиенту только после полной оплаты — ${money(total)}.`}
`;
}
@@ -422,11 +516,37 @@ function renderPayments(){
${left>0?`
⏰ Задача на остаток `:''}
${pays.length?`
⬇ CSV `:''}
${pays.map((p,i)=>`
${esc(p.date||'')} ${esc(p.note||'Платёж')} ${money(p.amount)} ✕
`).join("")||'
Платежей нет
'}
-
+ Платёж
+
+
+
+
+ — без этапа —
+ ${CLIENT_STAGES.filter(s=>stageIsDone(s.key)&&!getStagePays()[s.key]&&((getStagePrices()||{})[s.key]||0)>0).map(s=>`${s.name} · ${money((getStagePrices()||{})[s.key])} `).join('')}
+
+
+ + Платёж
+
`;
}
-async function savePayments(){await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,payments:state.crm.payments})});await loadProjects();}
-function addPayment(){const amt=+document.getElementById("payAmt").value;if(!amt){alert("Укажите сумму");return}const date=document.getElementById("payDate").value;const note=document.getElementById("payNote").value;state.crm=state.crm||{};state.crm.payments=state.crm.payments||[];state.crm.payments.push({date,amount:amt,note});savePayments();renderClient();}
+async function savePayments(){
+ await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,payments:state.crm.payments,stage_payments:state.crm.stage_payments||{}})});
+ await loadProjects();
+}
+function addPayment(){
+ const amt=+document.getElementById("payAmt").value;if(!amt){alert("Укажите сумму");return;}
+ const date=document.getElementById("payDate").value;
+ const note=document.getElementById("payNote").value;
+ const stageKey=document.getElementById("payStage")?.value||"";
+ const stageName=stageKey?CLIENT_STAGES.find(s=>s.key===stageKey)?.name:"";
+ state.crm=state.crm||{};
+ state.crm.payments=state.crm.payments||[];
+ state.crm.payments.push({date,amount:amt,note:note||(stageName?"Этап: "+stageName:"Платёж"),stage:stageKey||null});
+ if(stageKey&&!getStagePays()[stageKey]){
+ state.crm.stage_payments=state.crm.stage_payments||{};
+ state.crm.stage_payments[stageKey]={amount:amt,date};
+ }
+ savePayments();renderClient();
+}
function delPayment(i){state.crm.payments.splice(i,1);savePayments();renderClient();}
function remindBalance(left){
const due=new Date(Date.now()+7*864e5).toISOString().slice(0,10);