From a056f1a7eb08baf277bf80e16331e7722cfd0c48 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Sat, 30 May 2026 14:54:50 +0300 Subject: [PATCH] feat: revenue chart by month + auto-task on balance + payments CSV export --- docs/crm.html | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/crm.html b/docs/crm.html index 663dc83..96dd373 100644 --- a/docs/crm.html +++ b/docs/crm.html @@ -182,11 +182,24 @@ function renderDashboard(){
${money(expected)}
Ожидается (остатки)
${money(inwork)} в активных сделках
${conv}%
Конверсия в сделку
${done} завершено
+ ${renderRevenueChart()} ${renderUpcomingTasks()}
Все клиенты · ${total}
${projects.map(p=>{const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];return `
${esc((p.client_name||'?')[0])}
${esc(p.client_name)}
${esc(p.niche)} · ${p.msg_count} сообщений
${pc[1]}${money((p.crm&&p.crm.deal_amount)||0)}
`}).join("")||'
Создайте первого клиента
'}`; } +function renderRevenueChart(){ + const months={}; + projects.forEach(p=>(p.crm&&p.crm.payments||[]).forEach(pay=>{const m=(pay.date||"").slice(0,7);if(m)months[m]=(months[m]||0)+(pay.amount||0)})); + const keys=Object.keys(months).sort(); + if(!keys.length)return""; + const max=Math.max(...keys.map(k=>months[k])); + const MN=["янв","фев","мар","апр","май","июн","июл","авг","сен","окт","ноя","дек"]; + const lbl=k=>{const[y,m]=k.split("-");return MN[+m-1]+" "+y.slice(2)}; + const totalRev=keys.reduce((s,k)=>s+months[k],0); + return `
📈 Выручка по месяцам · ${money(totalRev)}
+
${keys.map(k=>`
${(months[k]/1000).toFixed(0)}к
${lbl(k)}
`).join("")}
`; +} function renderUpcomingTasks(){ const today=new Date().toISOString().slice(0,10); const all=[]; @@ -230,8 +243,10 @@ function renderPayments(){ 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)}`:''}
+
💰 Платежи + ${deal>0?`Сделка ${money(deal)} · Получено ${money(paid)} · Остаток ${money(left)}`:''} + ${left>0?``:''} + ${pays.length?``:''}
${pays.map((p,i)=>`
${esc(p.date||'')}${esc(p.note||'Платёж')}${money(p.amount)}
`).join("")||'
Платежей нет
'}
`; @@ -239,6 +254,19 @@ function renderPayments(){ 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 remindBalance(left){ + const due=new Date(Date.now()+7*864e5).toISOString().slice(0,10); + state.tasks=state.tasks||[];state.tasks.push({text:`Получить остаток ${money(left)} от ${state.client_name||'клиента'}`,due,done:false}); + saveTasks();renderTasks(); + alert(`Задача создана: «Получить остаток ${money(left)}»\nСрок: через 7 дней`); +} +function exportPayments(){ + const pays=state.crm.payments||[]; + let csv="Дата;Сумма;Назначение\n"+pays.map(p=>`${p.date||''};${p.amount||0};${(p.note||'').replace(/;/g,',')}`).join("\n"); + csv=""+csv; // BOM для Excel + const blob=new Blob([csv],{type:"text/csv;charset=utf-8"}); + const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=`Платежи_${(state.client_name||'клиент').replace(/[^а-яёa-z0-9]/gi,'_')}.csv`;a.click(); +} function renderTasks(){ const box=document.getElementById("tasksBox");if(!box)return; const tasks=state.tasks||[];