mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 21:04:47 +00:00
feat: payments registry — payment log, balance, status + finance KPIs on dashboard
This commit is contained in:
parent
c37491cbc7
commit
13db0cf9c9
@ -609,10 +609,13 @@ def update_crm():
|
|||||||
return jsonify({"error": "project not found"}), 404
|
return jsonify({"error": "project not found"}), 404
|
||||||
crm = latest_artifact(proj["id"], "crm") or {
|
crm = latest_artifact(proj["id"], "crm") or {
|
||||||
"pipeline": "lead", "deal_amount": 0, "paid_amount": 0,
|
"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]
|
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)
|
save_artifact(proj["id"], "crm", crm)
|
||||||
return jsonify({"ok": True, "crm": crm})
|
return jsonify({"ok": True, "crm": crm})
|
||||||
|
|
||||||
|
|||||||
@ -169,15 +169,17 @@ function renderDashboard(){
|
|||||||
const total=projects.length;
|
const total=projects.length;
|
||||||
const byPipe=k=>projects.filter(p=>((p.crm&&p.crm.pipeline)||"lead")===k).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 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 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;
|
const conv=total?Math.round(done/total*100):0;
|
||||||
document.getElementById("view").innerHTML=`
|
document.getElementById("view").innerHTML=`
|
||||||
<div class="sec-h">Дашборд</div>
|
<div class="sec-h">Дашборд</div>
|
||||||
<div class="kpis">
|
<div class="kpis">
|
||||||
<div class="kpi"><div class="kpi-v">${leads}</div><div class="kpi-l">Новых лидов</div></div>
|
<div class="kpi"><div class="kpi-v">${leads}</div><div class="kpi-l">Новых лидов</div><div class="kpi-sub">${active} в работе</div></div>
|
||||||
<div class="kpi"><div class="kpi-v">${active}</div><div class="kpi-l">Активных клиентов</div><div class="kpi-sub">${money(inwork)} в работе</div></div>
|
<div class="kpi"><div class="kpi-v">${money(revenue)}</div><div class="kpi-l">Выручка (получено)</div></div>
|
||||||
<div class="kpi"><div class="kpi-v">${money(revenue)}</div><div class="kpi-l">Выручка (оплачено)</div></div>
|
<div class="kpi"><div class="kpi-v">${money(expected)}</div><div class="kpi-l">Ожидается (остатки)</div><div class="kpi-sub">${money(inwork)} в активных сделках</div></div>
|
||||||
<div class="kpi"><div class="kpi-v">${conv}%</div><div class="kpi-l">Конверсия в сделку</div><div class="kpi-sub">${done} завершено</div></div>
|
<div class="kpi"><div class="kpi-v">${conv}%</div><div class="kpi-l">Конверсия в сделку</div><div class="kpi-sub">${done} завершено</div></div>
|
||||||
</div>
|
</div>
|
||||||
${renderUpcomingTasks()}
|
${renderUpcomingTasks()}
|
||||||
@ -208,16 +210,35 @@ function renderClient(){
|
|||||||
<div class="cc-grid">
|
<div class="cc-grid">
|
||||||
<div class="cc-field"><div class="cc-fl">Статус воронки</div><select class="cc-sel" id="cpPipe" onchange="saveCrm()">${PIPE.map(([k,n])=>`<option value="${k}" ${crm.pipeline===k?'selected':''}>${n}</option>`).join("")}</select></div>
|
<div class="cc-field"><div class="cc-fl">Статус воронки</div><select class="cc-sel" id="cpPipe" onchange="saveCrm()">${PIPE.map(([k,n])=>`<option value="${k}" ${crm.pipeline===k?'selected':''}>${n}</option>`).join("")}</select></div>
|
||||||
<div class="cc-field"><div class="cc-fl">Сумма сделки</div><input class="cc-fi" id="cpDeal" type="number" value="${crm.deal_amount||''}" placeholder="0" onchange="saveCrm()"></div>
|
<div class="cc-field"><div class="cc-fl">Сумма сделки</div><input class="cc-fi" id="cpDeal" type="number" value="${crm.deal_amount||''}" placeholder="0" onchange="saveCrm()"></div>
|
||||||
<div class="cc-field"><div class="cc-fl">Оплачено</div><input class="cc-fi" id="cpPaid" type="number" value="${crm.paid_amount||''}" placeholder="0" onchange="saveCrm()"></div>
|
|
||||||
<div class="cc-field"><div class="cc-fl">Источник</div><input class="cc-fi" id="cpSrc" value="${esc(crm.source)}" placeholder="откуда пришёл" onchange="saveCrm()"></div>
|
<div class="cc-field"><div class="cc-fl">Источник</div><input class="cc-fi" id="cpSrc" value="${esc(crm.source)}" placeholder="откуда пришёл" onchange="saveCrm()"></div>
|
||||||
|
<div class="cc-field" id="payStatusBox"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="paymentsBox"></div>
|
||||||
<div class="cc-actions"><button class="btn btn-p" onclick="inviteLink()">🔗 Ссылка клиенту</button><a class="btn btn-g" href="cabinet.html?t=${current}" target="_blank">👁 Открыть кабинет</a><button class="btn btn-g" style="margin-left:auto;border-color:#FECACA;color:#DC2626" onclick="deleteClient()">🗑 Удалить клиента</button></div>
|
<div class="cc-actions"><button class="btn btn-p" onclick="inviteLink()">🔗 Ссылка клиенту</button><a class="btn btn-g" href="cabinet.html?t=${current}" target="_blank">👁 Открыть кабинет</a><button class="btn btn-g" style="margin-left:auto;border-color:#FECACA;color:#DC2626" onclick="deleteClient()">🗑 Удалить клиента</button></div>
|
||||||
<div id="tasksBox"></div>
|
<div id="tasksBox"></div>
|
||||||
<div class="tabs">${TABS.map(t=>`<div class="tab ${t.id===activeTab?'active':''} ${approved(t.id)?'done':''}" onclick="setTab('${t.id}')">${t.icon} ${t.name}</div>`).join("")}</div>
|
<div class="tabs">${TABS.map(t=>`<div class="tab ${t.id===activeTab?'active':''} ${approved(t.id)?'done':''}" onclick="setTab('${t.id}')">${t.icon} ${t.name}</div>`).join("")}</div>
|
||||||
<div id="tabContent"></div>`;
|
<div id="tabContent"></div>`;
|
||||||
renderTasks();
|
renderTasks();
|
||||||
|
renderPayments();
|
||||||
renderTab();
|
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=`<div class="cc-fl">Оплата</div><div style="display:flex;align-items:center;gap:8px"><span style="font-size:14px;font-weight:700;color:${st[1]}">${money(paid)}</span>${deal>0?`<span style="font-size:11px;font-weight:700;color:${st[1]};background:${st[2]};padding:2px 8px;border-radius:6px">${st[0]}</span>`:''}</div>`;
|
||||||
|
const box=document.getElementById("paymentsBox");if(!box)return;
|
||||||
|
box.innerHTML=`<div style="background:var(--white);border:1.5px solid var(--border);border-radius:12px;padding:14px 16px;margin-bottom:18px">
|
||||||
|
<div style="display:flex;align-items:center;margin-bottom:10px"><span style="font-size:13px;font-weight:700">💰 Платежи</span>
|
||||||
|
${deal>0?`<span style="margin-left:auto;font-size:12px;color:var(--muted)">Сделка ${money(deal)} · Получено ${money(paid)} · <b style="color:${left>0?'#DC2626':'#047857'}">Остаток ${money(left)}</b></span>`:''}</div>
|
||||||
|
${pays.map((p,i)=>`<div style="display:flex;align-items:center;gap:10px;padding:7px 0;border-top:1px solid var(--bg)"><span style="font-size:12px;color:var(--muted);min-width:70px">${esc(p.date||'')}</span><span style="flex:1;font-size:13px">${esc(p.note||'Платёж')}</span><span style="font-size:13px;font-weight:700;color:#047857">${money(p.amount)}</span><button onclick="delPayment(${i})" style="border:none;background:none;cursor:pointer;color:#cbd5e1;font-size:14px">✕</button></div>`).join("")||'<div style="font-size:12px;color:#cbd5e1;padding:4px">Платежей нет</div>'}
|
||||||
|
<div style="display:flex;gap:8px;margin-top:10px"><input id="payDate" type="date" value="${new Date().toISOString().slice(0,10)}" style="border:1.5px solid var(--border);border-radius:8px;padding:8px;font-size:13px;font-family:Inter"><input id="payAmt" type="number" placeholder="Сумма ₽" style="width:120px;border:1.5px solid var(--border);border-radius:8px;padding:8px 11px;font-size:13px;font-family:Inter" onkeydown="if(event.key==='Enter')addPayment()"><input id="payNote" placeholder="Назначение (аванс, постоплата...)" style="flex:1;border:1.5px solid var(--border);border-radius:8px;padding:8px 11px;font-size:13px;font-family:Inter" onkeydown="if(event.key==='Enter')addPayment()"><button class="cp-btn cp-a" onclick="addPayment()">+ Платёж</button></div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
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(){
|
function renderTasks(){
|
||||||
const box=document.getElementById("tasksBox");if(!box)return;
|
const box=document.getElementById("tasksBox");if(!box)return;
|
||||||
const tasks=state.tasks||[];
|
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 toggleTask(i){state.tasks[i].done=!state.tasks[i].done;renderTasks();saveTasks();}
|
||||||
function delTask(i){state.tasks.splice(i,1);renderTasks();saveTasks();}
|
function delTask(i){state.tasks.splice(i,1);renderTasks();saveTasks();}
|
||||||
async function saveCrm(){
|
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};
|
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 fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,...crm})});
|
||||||
await loadProjects();
|
await loadProjects();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user