diff --git a/backend/elena_app.py b/backend/elena_app.py index 70eadb4..f186ebc 100644 --- a/backend/elena_app.py +++ b/backend/elena_app.py @@ -609,10 +609,13 @@ def update_crm(): return jsonify({"error": "project not found"}), 404 crm = latest_artifact(proj["id"], "crm") or { "pipeline": "lead", "deal_amount": 0, "paid_amount": 0, - "contact": "", "source": "", "note": "" + "contact": "", "source": "", "note": "", "payments": [] } - for k in ("pipeline", "deal_amount", "paid_amount", "contact", "source", "note"): + for k in ("pipeline", "deal_amount", "paid_amount", "contact", "source", "note", "payments"): if k in data: crm[k] = data[k] + # paid_amount = сумма платежей (если есть реестр) + if "payments" in crm and isinstance(crm["payments"], list): + crm["paid_amount"] = sum(p.get("amount", 0) for p in crm["payments"]) save_artifact(proj["id"], "crm", crm) return jsonify({"ok": True, "crm": crm}) diff --git a/docs/crm.html b/docs/crm.html index d9a4957..663dc83 100644 --- a/docs/crm.html +++ b/docs/crm.html @@ -169,15 +169,17 @@ function renderDashboard(){ const total=projects.length; const byPipe=k=>projects.filter(p=>((p.crm&&p.crm.pipeline)||"lead")===k).length; const active=byPipe("active"),done=byPipe("done"),leads=byPipe("lead"); - const revenue=projects.reduce((s,p)=>s+((p.crm&&p.crm.paid_amount)||0),0); + const paidOf=p=>{const c=p.crm||{};return (c.payments||[]).reduce((s,x)=>s+(x.amount||0),0)||c.paid_amount||0}; + const revenue=projects.reduce((s,p)=>s+paidOf(p),0); + const expected=projects.reduce((s,p)=>{const c=p.crm||{};return s+Math.max(0,(c.deal_amount||0)-paidOf(p))},0); const inwork=projects.filter(p=>((p.crm&&p.crm.pipeline)||"")==="active").reduce((s,p)=>s+((p.crm&&p.crm.deal_amount)||0),0); const conv=total?Math.round(done/total*100):0; document.getElementById("view").innerHTML=`
Дашборд
-
${leads}
Новых лидов
-
${active}
Активных клиентов
${money(inwork)} в работе
-
${money(revenue)}
Выручка (оплачено)
+
${leads}
Новых лидов
${active} в работе
+
${money(revenue)}
Выручка (получено)
+
${money(expected)}
Ожидается (остатки)
${money(inwork)} в активных сделках
${conv}%
Конверсия в сделку
${done} завершено
${renderUpcomingTasks()} @@ -208,16 +210,35 @@ function renderClient(){
Статус воронки
Сумма сделки
-
Оплачено
Источник
+
+
👁 Открыть кабинет
${TABS.map(t=>`
${t.icon} ${t.name}
`).join("")}
`; renderTasks(); + renderPayments(); renderTab(); } +function renderPayments(){ + const crm=state.crm||{};const deal=crm.deal_amount||0;const pays=crm.payments||[]; + const paid=pays.reduce((s,p)=>s+(p.amount||0),0);const left=deal-paid; + const st=paid<=0?["Не оплачено","#DC2626","#FEF2F2"]:left>0?["Частично","#92400E","#FEF3C7"]:["Оплачено","#047857","#ECFDF5"]; + const sb=document.getElementById("payStatusBox"); + if(sb)sb.innerHTML=`
Оплата
${money(paid)}${deal>0?`${st[0]}`:''}
`; + const box=document.getElementById("paymentsBox");if(!box)return; + box.innerHTML=`
+
💰 Платежи + ${deal>0?`Сделка ${money(deal)} · Получено ${money(paid)} · Остаток ${money(left)}`:''}
+ ${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});renderPayments();savePayments();} +function delPayment(i){state.crm.payments.splice(i,1);renderPayments();savePayments();} function renderTasks(){ const box=document.getElementById("tasksBox");if(!box)return; const tasks=state.tasks||[]; @@ -234,7 +255,7 @@ function addTask(){const t=document.getElementById("newTask").value.trim();if(!t function toggleTask(i){state.tasks[i].done=!state.tasks[i].done;renderTasks();saveTasks();} function delTask(i){state.tasks.splice(i,1);renderTasks();saveTasks();} async function saveCrm(){ - const crm={pipeline:document.getElementById("cpPipe").value,deal_amount:+document.getElementById("cpDeal").value||0,paid_amount:+document.getElementById("cpPaid").value||0,source:document.getElementById("cpSrc").value}; + const crm={pipeline:document.getElementById("cpPipe").value,deal_amount:+document.getElementById("cpDeal").value||0,source:document.getElementById("cpSrc").value}; state.crm={...state.crm,...crm}; await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,...crm})}); await loadProjects();