mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 15:44:45 +00:00
crm dashboard: client rows show billing type + 5-stage stepper + current stage; backend adds has_selection
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
37c7486cc0
commit
9a349fe21e
@ -191,7 +191,46 @@ function renderDashboard(){
|
|||||||
${renderRevenueChart()}
|
${renderRevenueChart()}
|
||||||
${renderUpcomingTasks()}
|
${renderUpcomingTasks()}
|
||||||
<div class="sec-h">Все клиенты · ${total}</div>
|
<div class="sec-h">Все клиенты · ${total}</div>
|
||||||
${projects.map(p=>{const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];return `<div class="blk" style="display:flex;align-items:center;gap:14px;cursor:pointer" onclick="openClient('${p.token}')"><div class="cl-av" style="width:36px;height:36px;border-radius:9px;font-size:14px">${esc((p.client_name||'?')[0])}</div><div style="flex:1"><div style="font-weight:700">${esc(p.client_name)}</div><div style="font-size:12px;color:var(--muted)">${esc(p.niche)} · ${p.msg_count} сообщений</div></div><span style="font-size:11px;font-weight:700;color:${pc[2]};background:${pc[2]}1a;padding:4px 10px;border-radius:20px">${pc[1]}</span><span style="font-weight:700;color:var(--primary)">${money((p.crm&&p.crm.deal_amount)||0)}</span></div>`}).join("")||'<div class="empty">Создайте первого клиента</div>'}`;
|
${projects.map(p=>renderClientRow(p)).join("")||'<div class="empty">Создайте первого клиента</div>'}`;
|
||||||
|
}
|
||||||
|
const STAGE_DEFS=[{key:"interview",name:"Интервью"},{key:"methods",name:"Методологии"},{key:"canvas",name:"Стратегия"},{key:"idef0",name:"Функции"},{key:"spec",name:"ТЗ"}];
|
||||||
|
function clientStages(p){
|
||||||
|
const done=[p.msg_count>0,!!p.has_selection,!!p.has_canvas,!!p.has_idef0,!!p.has_spec];
|
||||||
|
const cnt=done.filter(Boolean).length;
|
||||||
|
let cur=done.findIndex(d=>!d); // первый незавершённый
|
||||||
|
const all=cur===-1;
|
||||||
|
const label=all?"Готово · ТЗ собрано":STAGE_DEFS[cur].name;
|
||||||
|
return {done,cnt,cur:all?STAGE_DEFS.length:cur,all,label};
|
||||||
|
}
|
||||||
|
function stepper(p){
|
||||||
|
const st=clientStages(p);
|
||||||
|
let dots="";
|
||||||
|
STAGE_DEFS.forEach((s,i)=>{
|
||||||
|
const isDone=st.done[i], isCur=!st.all&&i===st.cur;
|
||||||
|
const bg=isDone?"#047857":isCur?"#10B981":"#E5E7EB";
|
||||||
|
const ring=isCur?"box-shadow:0 0 0 3px rgba(16,185,129,.25);":"";
|
||||||
|
dots+=`<span title="${s.name}${isDone?' ✓':isCur?' — текущий':''}" style="width:11px;height:11px;border-radius:50%;background:${bg};${ring}flex-shrink:0;display:inline-block"></span>`;
|
||||||
|
if(i<STAGE_DEFS.length-1){const lc=st.done[i]?"#047857":"#E5E7EB";dots+=`<span style="height:2px;flex:1;min-width:14px;background:${lc};display:inline-block"></span>`;}
|
||||||
|
});
|
||||||
|
const col=st.all?"#047857":"#10B981";
|
||||||
|
return `<div style="display:flex;align-items:center;gap:0;margin-top:8px;max-width:360px">${dots}</div>
|
||||||
|
<div style="font-size:11px;color:var(--muted);margin-top:4px">Этап: <b style="color:${col}">${st.label}</b> · ${st.cnt}/5</div>`;
|
||||||
|
}
|
||||||
|
function renderClientRow(p){
|
||||||
|
const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];
|
||||||
|
const billing=(p.crm&&p.crm.billing_type)||"paid";
|
||||||
|
const bChip=billing==="free"
|
||||||
|
?`<span style="font-size:10px;font-weight:700;color:#6366F1;background:#EEF2FF;padding:2px 8px;border-radius:6px">🎁 Бесплатный</span>`
|
||||||
|
:`<span style="font-size:10px;font-weight:700;color:#047857;background:#ECFDF5;padding:2px 8px;border-radius:6px">💰 Платный</span>`;
|
||||||
|
return `<div class="blk" style="display:flex;align-items:flex-start;gap:14px;cursor:pointer" onclick="openClient('${p.token}')">
|
||||||
|
<div class="cl-av" style="width:36px;height:36px;border-radius:9px;font-size:14px;margin-top:2px">${esc((p.client_name||'?')[0])}</div>
|
||||||
|
<div style="flex:1;min-width:0">
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap"><span style="font-weight:700">${esc(p.client_name)}</span>${bChip}</div>
|
||||||
|
<div style="font-size:12px;color:var(--muted);margin-top:1px">${esc(p.niche)} · ${p.msg_count} сообщений</div>
|
||||||
|
${stepper(p)}
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:6px"><span style="font-size:11px;font-weight:700;color:${pc[2]};background:${pc[2]}1a;padding:4px 10px;border-radius:20px;white-space:nowrap">${pc[1]}</span><span style="font-weight:700;color:var(--primary)">${money((p.crm&&p.crm.deal_amount)||0)}</span></div>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRevenueChart(){
|
function renderRevenueChart(){
|
||||||
|
|||||||
@ -952,6 +952,7 @@ def list_projects():
|
|||||||
"token": r["token"], "client_name": r["client_name"] or "Без имени",
|
"token": r["token"], "client_name": r["client_name"] or "Без имени",
|
||||||
"niche": r["niche"] or "", "status": r["status"],
|
"niche": r["niche"] or "", "status": r["status"],
|
||||||
"created_at": r["created_at"], "msg_count": r["msg_count"],
|
"created_at": r["created_at"], "msg_count": r["msg_count"],
|
||||||
|
"has_selection": latest_artifact(pid, "selection") is not None,
|
||||||
"has_canvas": latest_artifact(pid, "canvas") is not None,
|
"has_canvas": latest_artifact(pid, "canvas") is not None,
|
||||||
"has_idef0": db().execute("SELECT 1 FROM models WHERE project_id=? LIMIT 1", (pid,)).fetchone() is not None,
|
"has_idef0": db().execute("SELECT 1 FROM models WHERE project_id=? LIMIT 1", (pid,)).fetchone() is not None,
|
||||||
"has_spec": latest_artifact(pid, "spec") is not None,
|
"has_spec": latest_artifact(pid, "spec") is not None,
|
||||||
|
|||||||
@ -191,7 +191,46 @@ function renderDashboard(){
|
|||||||
${renderRevenueChart()}
|
${renderRevenueChart()}
|
||||||
${renderUpcomingTasks()}
|
${renderUpcomingTasks()}
|
||||||
<div class="sec-h">Все клиенты · ${total}</div>
|
<div class="sec-h">Все клиенты · ${total}</div>
|
||||||
${projects.map(p=>{const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];return `<div class="blk" style="display:flex;align-items:center;gap:14px;cursor:pointer" onclick="openClient('${p.token}')"><div class="cl-av" style="width:36px;height:36px;border-radius:9px;font-size:14px">${esc((p.client_name||'?')[0])}</div><div style="flex:1"><div style="font-weight:700">${esc(p.client_name)}</div><div style="font-size:12px;color:var(--muted)">${esc(p.niche)} · ${p.msg_count} сообщений</div></div><span style="font-size:11px;font-weight:700;color:${pc[2]};background:${pc[2]}1a;padding:4px 10px;border-radius:20px">${pc[1]}</span><span style="font-weight:700;color:var(--primary)">${money((p.crm&&p.crm.deal_amount)||0)}</span></div>`}).join("")||'<div class="empty">Создайте первого клиента</div>'}`;
|
${projects.map(p=>renderClientRow(p)).join("")||'<div class="empty">Создайте первого клиента</div>'}`;
|
||||||
|
}
|
||||||
|
const STAGE_DEFS=[{key:"interview",name:"Интервью"},{key:"methods",name:"Методологии"},{key:"canvas",name:"Стратегия"},{key:"idef0",name:"Функции"},{key:"spec",name:"ТЗ"}];
|
||||||
|
function clientStages(p){
|
||||||
|
const done=[p.msg_count>0,!!p.has_selection,!!p.has_canvas,!!p.has_idef0,!!p.has_spec];
|
||||||
|
const cnt=done.filter(Boolean).length;
|
||||||
|
let cur=done.findIndex(d=>!d); // первый незавершённый
|
||||||
|
const all=cur===-1;
|
||||||
|
const label=all?"Готово · ТЗ собрано":STAGE_DEFS[cur].name;
|
||||||
|
return {done,cnt,cur:all?STAGE_DEFS.length:cur,all,label};
|
||||||
|
}
|
||||||
|
function stepper(p){
|
||||||
|
const st=clientStages(p);
|
||||||
|
let dots="";
|
||||||
|
STAGE_DEFS.forEach((s,i)=>{
|
||||||
|
const isDone=st.done[i], isCur=!st.all&&i===st.cur;
|
||||||
|
const bg=isDone?"#047857":isCur?"#10B981":"#E5E7EB";
|
||||||
|
const ring=isCur?"box-shadow:0 0 0 3px rgba(16,185,129,.25);":"";
|
||||||
|
dots+=`<span title="${s.name}${isDone?' ✓':isCur?' — текущий':''}" style="width:11px;height:11px;border-radius:50%;background:${bg};${ring}flex-shrink:0;display:inline-block"></span>`;
|
||||||
|
if(i<STAGE_DEFS.length-1){const lc=st.done[i]?"#047857":"#E5E7EB";dots+=`<span style="height:2px;flex:1;min-width:14px;background:${lc};display:inline-block"></span>`;}
|
||||||
|
});
|
||||||
|
const col=st.all?"#047857":"#10B981";
|
||||||
|
return `<div style="display:flex;align-items:center;gap:0;margin-top:8px;max-width:360px">${dots}</div>
|
||||||
|
<div style="font-size:11px;color:var(--muted);margin-top:4px">Этап: <b style="color:${col}">${st.label}</b> · ${st.cnt}/5</div>`;
|
||||||
|
}
|
||||||
|
function renderClientRow(p){
|
||||||
|
const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];
|
||||||
|
const billing=(p.crm&&p.crm.billing_type)||"paid";
|
||||||
|
const bChip=billing==="free"
|
||||||
|
?`<span style="font-size:10px;font-weight:700;color:#6366F1;background:#EEF2FF;padding:2px 8px;border-radius:6px">🎁 Бесплатный</span>`
|
||||||
|
:`<span style="font-size:10px;font-weight:700;color:#047857;background:#ECFDF5;padding:2px 8px;border-radius:6px">💰 Платный</span>`;
|
||||||
|
return `<div class="blk" style="display:flex;align-items:flex-start;gap:14px;cursor:pointer" onclick="openClient('${p.token}')">
|
||||||
|
<div class="cl-av" style="width:36px;height:36px;border-radius:9px;font-size:14px;margin-top:2px">${esc((p.client_name||'?')[0])}</div>
|
||||||
|
<div style="flex:1;min-width:0">
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap"><span style="font-weight:700">${esc(p.client_name)}</span>${bChip}</div>
|
||||||
|
<div style="font-size:12px;color:var(--muted);margin-top:1px">${esc(p.niche)} · ${p.msg_count} сообщений</div>
|
||||||
|
${stepper(p)}
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:6px"><span style="font-size:11px;font-weight:700;color:${pc[2]};background:${pc[2]}1a;padding:4px 10px;border-radius:20px;white-space:nowrap">${pc[1]}</span><span style="font-weight:700;color:var(--primary)">${money((p.crm&&p.crm.deal_amount)||0)}</span></div>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRevenueChart(){
|
function renderRevenueChart(){
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user