diff --git a/Mokap/crm.html b/Mokap/crm.html index c8f9fd8..6ba7956 100644 --- a/Mokap/crm.html +++ b/Mokap/crm.html @@ -191,7 +191,46 @@ function renderDashboard(){ ${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("")||'
Создайте первого клиента
'}`; + ${projects.map(p=>renderClientRow(p)).join("")||'
Создайте первого клиента
'}`; +} +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+=``; + if(i`;} + }); + const col=st.all?"#047857":"#10B981"; + return `
${dots}
+
Этап: ${st.label} · ${st.cnt}/5
`; +} +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" + ?`🎁 Бесплатный` + :`💰 Платный`; + return `
+
${esc((p.client_name||'?')[0])}
+
+
${esc(p.client_name)}${bChip}
+
${esc(p.niche)} · ${p.msg_count} сообщений
+ ${stepper(p)} +
+
${pc[1]}${money((p.crm&&p.crm.deal_amount)||0)}
+
`; } function renderRevenueChart(){ diff --git a/backend/elena_app.py b/backend/elena_app.py index de5e9c4..567b173 100644 --- a/backend/elena_app.py +++ b/backend/elena_app.py @@ -952,6 +952,7 @@ def list_projects(): "token": r["token"], "client_name": r["client_name"] or "Без имени", "niche": r["niche"] or "", "status": r["status"], "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_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, diff --git a/docs/crm.html b/docs/crm.html index c8f9fd8..6ba7956 100644 --- a/docs/crm.html +++ b/docs/crm.html @@ -191,7 +191,46 @@ function renderDashboard(){ ${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("")||'
Создайте первого клиента
'}`; + ${projects.map(p=>renderClientRow(p)).join("")||'
Создайте первого клиента
'}`; +} +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+=``; + if(i`;} + }); + const col=st.all?"#047857":"#10B981"; + return `
${dots}
+
Этап: ${st.label} · ${st.cnt}/5
`; +} +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" + ?`🎁 Бесплатный` + :`💰 Платный`; + return `
+
${esc((p.client_name||'?')[0])}
+
+
${esc(p.client_name)}${bChip}
+
${esc(p.niche)} · ${p.msg_count} сообщений
+ ${stepper(p)} +
+
${pc[1]}${money((p.crm&&p.crm.deal_amount)||0)}
+
`; } function renderRevenueChart(){