feat(воронка): drag-and-drop смены этапа, суммы по колонкам, степпер+бейдж на картах, моб.скролл

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
wasrusgen 2026-06-01 22:59:40 +03:00
parent 0a9d924d58
commit 30160a0999

View File

@ -66,6 +66,10 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
.kcard-n{font-size:13px;font-weight:700;margin-bottom:3px} .kcard-n{font-size:13px;font-weight:700;margin-bottom:3px}
.kcard-m{font-size:11px;color:var(--muted)} .kcard-m{font-size:11px;color:var(--muted)}
.kcard-amt{font-size:12px;font-weight:700;color:var(--primary);margin-top:6px} .kcard-amt{font-size:12px;font-weight:700;color:var(--primary);margin-top:6px}
.kcol-sum{font-size:11px;font-weight:700;color:var(--primary);padding:0 8px 4px}
.kcard[draggable=true]{cursor:grab}
.kcard.dragging{opacity:.45;cursor:grabbing}
.kcol.drop-hot{background:#ECFDF5;outline:2px dashed var(--mid);outline-offset:-2px}
/* Client card */ /* Client card */
.cc-top{display:flex;align-items:center;gap:14px;margin-bottom:18px} .cc-top{display:flex;align-items:center;gap:14px;margin-bottom:18px}
.cc-av{width:48px;height:48px;border-radius:12px;background:#6366F1;display:flex;align-items:center;justify-content:center;font-weight:700;color:#fff;font-size:20px} .cc-av{width:48px;height:48px;border-radius:12px;background:#6366F1;display:flex;align-items:center;justify-content:center;font-weight:700;color:#fff;font-size:20px}
@ -152,6 +156,7 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
.cc-top{flex-wrap:wrap} .cc-top{flex-wrap:wrap}
.cc-name{font-size:17px} .cc-name{font-size:17px}
.sec-h{font-size:14px} .sec-h{font-size:14px}
.kcol{min-width:168px}
} }
</style> </style>
</head> </head>
@ -369,10 +374,44 @@ function renderUpcomingTasks(){
return head+`<div class="tbl">${top.map(t=>`<div class="tbl-row" onclick="openClient('${t.token}')"><span style="font-size:13px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(t.text)}</span><span style="font-size:12px;color:var(--muted);white-space:nowrap">${esc(t.client)}</span>${t.due?`<span style="font-size:11px;font-weight:700;padding:3px 9px;border-radius:6px;white-space:nowrap;${t.due<today?'background:#FEF2F2;color:#DC2626':t.due===today?'background:#FEF3C7;color:#92400E':'background:#F1F5F9;color:#6B7280'}">${t.due===today?'сегодня':t.due<today?'просрочено':fmtDate(t.due)}</span>`:''}</div>`).join("")}</div>`; return head+`<div class="tbl">${top.map(t=>`<div class="tbl-row" onclick="openClient('${t.token}')"><span style="font-size:13px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(t.text)}</span><span style="font-size:12px;color:var(--muted);white-space:nowrap">${esc(t.client)}</span>${t.due?`<span style="font-size:11px;font-weight:700;padding:3px 9px;border-radius:6px;white-space:nowrap;${t.due<today?'background:#FEF2F2;color:#DC2626':t.due===today?'background:#FEF3C7;color:#92400E':'background:#F1F5F9;color:#6B7280'}">${t.due===today?'сегодня':t.due<today?'просрочено':fmtDate(t.due)}</span>`:''}</div>`).join("")}</div>`;
} }
function renderPipeline(){ function renderPipeline(){
document.getElementById("view").innerHTML=`<div class="sec-h">Воронка продаж</div><div class="kanban">${PIPE.map(([k,name,col])=>{ document.getElementById("view").innerHTML=`<div class="sec-h">Воронка продаж</div>
const items=projects.filter(p=>((p.crm&&p.crm.pipeline)||"lead")===k); <div style="font-size:12px;color:var(--muted);margin:-3px 0 10px">Перетащите карточку между колонками, чтобы сменить этап сделки</div>
return `<div class="kcol"><div class="kcol-h"><span style="color:${col}">${name}</span><span class="kcol-c">${items.length}</span></div>${items.map(p=>`<div class="kcard" onclick="openClient('${p.token}')"><div class="kcard-n">${esc(p.client_name)}</div><div class="kcard-m">${esc(p.niche)}</div>${(p.crm&&p.crm.deal_amount)?`<div class="kcard-amt">${money(p.crm.deal_amount)}</div>`:''}</div>`).join("")}</div>`; <div class="kanban">${PIPE.map(([k,name,col])=>{
}).join("")}</div>`; const items=projects.filter(p=>((p.crm&&p.crm.pipeline)||"lead")===k);
const sum=items.reduce((s,p)=>s+((p.crm&&p.crm.deal_amount)||0),0);
return `<div class="kcol" data-pipe="${k}" ondragover="kDragOver(event)" ondragleave="kDragLeave(event)" ondrop="kDrop(event,'${k}')">
<div class="kcol-h"><span style="color:${col}">${name}</span><span class="kcol-c">${items.length}</span></div>
${sum>0?`<div class="kcol-sum">${money(sum)}</div>`:''}
${items.map(p=>kCard(p)).join("")||'<div style="font-size:11px;color:#cbd5e1;text-align:center;padding:16px 4px">пусто</div>'}
</div>`;
}).join("")}</div>`;
}
function kCard(p){
const billing=(p.crm&&p.crm.billing_type)||"paid";
const bch=billing==='free'?'<span class="mini-chip" style="color:#6366F1;background:#EEF2FF">🎁</span>':'<span class="mini-chip" style="color:#047857;background:#ECFDF5">💰</span>';
const st=clientStages(p);
let dots="";STAGE_DEFS.forEach((s,i)=>{const d=st.done[i],c=!st.all&&i===st.cur;dots+=`<span title="${s.name}" style="width:7px;height:7px;border-radius:50%;background:${d?'#047857':c?'#10B981':'#E5E7EB'};display:inline-block"></span>`;});
return `<div class="kcard" draggable="true" ondragstart="kDragStart(event,'${p.token}')" ondragend="kDragEnd(event)" onclick="openClient('${p.token}')">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:3px"><span class="kcard-n" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(p.client_name)}</span>${bch}</div>
<div class="kcard-m">${esc(p.niche)}</div>
<div style="display:flex;align-items:center;gap:3px;margin-top:7px">${dots}<span style="font-size:10px;color:#9ca3af;margin-left:4px">${st.cnt}/5</span></div>
${(p.crm&&p.crm.deal_amount)?`<div class="kcard-amt">${money(p.crm.deal_amount)}</div>`:''}
</div>`;
}
let kDragToken=null;
function kDragStart(e,token){kDragToken=token;e.currentTarget.classList.add('dragging');try{e.dataTransfer.effectAllowed='move';e.dataTransfer.setData('text/plain',token);}catch(_){}}
function kDragEnd(e){e.currentTarget.classList.remove('dragging');document.querySelectorAll('.kcol').forEach(c=>c.classList.remove('drop-hot'));}
function kDragOver(e){e.preventDefault();e.currentTarget.classList.add('drop-hot');try{e.dataTransfer.dropEffect='move';}catch(_){}}
function kDragLeave(e){e.currentTarget.classList.remove('drop-hot');}
async function kDrop(e,pipe){
e.preventDefault();e.currentTarget.classList.remove('drop-hot');
const tok=kDragToken||(e.dataTransfer&&e.dataTransfer.getData('text/plain'));kDragToken=null;
if(!tok)return;
const p=projects.find(x=>x.token===tok);if(!p)return;
if(((p.crm&&p.crm.pipeline)||'lead')===pipe){renderPipeline();return;}
p.crm=p.crm||{};p.crm.pipeline=pipe;
await fetch(`${API}/api/project/crm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:tok,pipeline:pipe})});
await loadProjects();renderPipeline();
} }
const MAINTABS=[{id:"deal",name:"Сделка",icon:"📇"},{id:"pricing",name:"Ценообразование",icon:"💰"},{id:"payments",name:"Платежи",icon:"💳"},{id:"tasks",name:"Задачи",icon:"📌"},{id:"analysis",name:"Анализ",icon:"📊"}]; const MAINTABS=[{id:"deal",name:"Сделка",icon:"📇"},{id:"pricing",name:"Ценообразование",icon:"💰"},{id:"payments",name:"Платежи",icon:"💳"},{id:"tasks",name:"Задачи",icon:"📌"},{id:"analysis",name:"Анализ",icon:"📊"}];