From 5b146fb7e53c6e12517bfa707c4ee2a176be8d5c Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Mon, 1 Jun 2026 07:27:31 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=81=D0=BC=D0=B5=D1=82=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0=20=E2=80=94=20=D0=B3=D0=B8?= =?UTF-8?q?=D0=B1=D1=80=D0=B8=D0=B4=D0=BD=D0=B0=D1=8F=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=20=D0=BE=D0=BF=D0=BB=D0=B0=D1=82=D1=8B=20(?= =?UTF-8?q?=D0=B2=D1=85=D0=BE=D0=B4=200=E2=82=BD=20+=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D1=83=D0=BB=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/cabinet.html | 53 ++++++++++++- docs/crm.html | 184 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 201 insertions(+), 36 deletions(-) diff --git a/docs/cabinet.html b/docs/cabinet.html index 29d1849..1d1963f 100644 --- a/docs/cabinet.html +++ b/docs/cabinet.html @@ -511,9 +511,54 @@ function renderSpecPane(){ pad.innerHTML=runCard("spec","📋","Техническое задание","Из модели бизнеса Елена спроектирует программу: роли, модули, экраны, данные.","Собрать ТЗ →");return} pad.innerHTML=renderSpec(state.spec)+renderExportBar(); } +const EST_STAGES=[ + {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:"Готовое ТЗ к внедрению"}, +]; +function estStageDone(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; +} +function renderEstimateCard(){ + const crm=state.crm||{}; + const sp=crm.stage_prices; + if(!sp)return ''; // смета ещё не сформирована — не показываем + const pays=crm.stage_payments||{}; + const total=Object.values(sp).reduce((a,b)=>a+(+b||0),0); + const paid=Object.values(pays).reduce((s,p)=>s+(p.amount||0),0); + return `
+
🧾 Смета проекта
+
Прозрачно: за что и сколько. Интервью — бесплатно, дальше помодульно. Готовое ТЗ — после полной оплаты.
+ ${EST_STAGES.map(s=>{ + const price=+sp[s.key]||0, isFree=price<=0, isPaid=!!pays[s.key], done=estStageDone(s.key); + let tag=''; + if(isFree)tag='бесплатно'; + else if(isPaid)tag='✓ оплачено'; + else if(done)tag='к оплате'; + else tag='в работе'; + return `
+ ${s.icon} +
${s.name} ${tag}
${s.desc}
+ ${isFree?'0 ₽':money(price)} +
`; + }).join('')} +
+ Итого + ${money(total)} +
+ ${paid>0?`
Оплачено: ${money(paid)} · остаток ${money(Math.max(0,total-paid))}
`:''} +
`; +} function renderExportBar(){ if(state.unlocked){ - return `
📦 Готовые документы
+ return renderEstimateCard()+`
📦 Готовые документы
Долг закрыт — документы и ТЗ доступны для печати и выгрузки.
@@ -522,10 +567,10 @@ function renderExportBar(){
`; } const debt=state.debt||0, deal=state.deal_amount||0; - const money=deal>0?`Сумма: ${deal.toLocaleString('ru')} ₽${debt>0?` · к оплате: ${debt.toLocaleString('ru')} ₽`:''}`:''; - return `
+ 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=>``).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=``; + } 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?``:''}
${pays.map((p,i)=>`
${esc(p.date||'')}${esc(p.note||'Платёж')}${money(p.amount)}
`).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);